From fd9674e46020d995da49cf4cce99dcbf8e3f582b Mon Sep 17 00:00:00 2001 From: BryceSmith494 Date: Mon, 10 Nov 2025 14:15:41 +0800 Subject: [PATCH 01/35] a test commit --- zdm/io.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zdm/io.py b/zdm/io.py index df99f331..2f7fecff 100644 --- a/zdm/io.py +++ b/zdm/io.py @@ -55,7 +55,7 @@ def savejson(filename, obj, overwrite=False, indent=None, easy_to_read=False, json.dump(obj, fh, indent=indent, **kwargs) ######### misc function to load some data - do we ever use it? ########## - +#random def load_data(filename): if filename.endswith('.npy'): data=np.load(filename) From d5ba3689514ed7decf7049431e34768b24a30156 Mon Sep 17 00:00:00 2001 From: BryceSmith494 Date: Fri, 21 Nov 2025 09:49:16 +0800 Subject: [PATCH 02/35] first z-smear implementation --- zdm/grid.py | 19 ++++++++++++++++++- zdm/parameters.py | 14 ++++++++++++++ .../plot_ASKAP_ICS.py | 19 +++++++++++++++---- 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/zdm/grid.py b/zdm/grid.py index 8db2bce8..03a47c7f 100644 --- a/zdm/grid.py +++ b/zdm/grid.py @@ -1,6 +1,8 @@ from IPython.terminal.embed import embed import numpy as np import datetime +from numpy import random +import scipy.ndimage as ndimage from zdm import cosmology as cos from zdm import misc_functions @@ -78,6 +80,7 @@ def __init__(self, survey, state, zDMgrid, zvals, dmvals, smear_mask, wdist=None self.dV = prev_grid.dV.copy() self.smear = prev_grid.smear.copy() self.smear_grid = prev_grid.smear_grid.copy() + if wdist is not None: efficiencies = survey.efficiencies # two OR three dimensions @@ -494,6 +497,10 @@ def calc_rates(self): self.rates = self.pdv * self.sfr_smear + if self.state.photo.smearing is True: + self.smear_z() + self.rates=self.smear_zgrid + def calc_thresholds(self, F0:float, eff_table, bandwidth=1e9, @@ -1166,4 +1173,14 @@ def chk_upd_param(self, param: str, vparams: dict, update=False): # return updated - +######################################## + + def smear_z(self): + r,c=self.rates.shape + smear_size=int(self.state.photo.sigma_width*self.state.photo.sigma*len(self.zvals)/max(self.zvals))+1 + smear_arr=random.normal(loc=1,scale=self.state.photo.sigma,size=(smear_size)) + smear_arr/=np.sum(smear_arr) + if not hasattr(self,"smear_zgrid"): + self.smear_zgrid=np.zeros([r,c]) + for i in range(c): + self.smear_zgrid[:,i]=np.convolve(self.rates[:,i],smear_arr,mode="same") diff --git a/zdm/parameters.py b/zdm/parameters.py index 0d5414f9..9c5195fc 100644 --- a/zdm/parameters.py +++ b/zdm/parameters.py @@ -414,6 +414,7 @@ def set_dataclasses(self): self.IGM = IGMParams() self.energy = EnergeticsParams() self.rep = RepeatParams() + self.photo=PhotometricParams() def update_param(self, param:str, value): # print(self.params) @@ -439,3 +440,16 @@ def set_astropy_cosmo(self, cosmo): self.cosmo.Omega_b = cosmo.Ob0 self.cosmo.Omega_b_h2 = cosmo.Ob0 * (cosmo.H0.value / 100.0) ** 2 return + +################################################################################ +@dataclass +class PhotometricParams(data_class.myDataClass): + + smearing:bool =field(default=False) + + sigma:float =field(default=0.035) + + sigma_width:int =field(default=6) + + + diff --git a/zdm/scripts/PlotIndividualExperiments/plot_ASKAP_ICS.py b/zdm/scripts/PlotIndividualExperiments/plot_ASKAP_ICS.py index a61356d8..d3bb6a86 100644 --- a/zdm/scripts/PlotIndividualExperiments/plot_ASKAP_ICS.py +++ b/zdm/scripts/PlotIndividualExperiments/plot_ASKAP_ICS.py @@ -58,17 +58,28 @@ def main(): # set limits for plots - will be LARGE! DMmax=3000 zmax=3. - # gets sum of rates over three sets of observations # weights by constant and TOBS time=0 + ########################### + samples=gs[1].GenMCSample(100) + FRBZs=np.zeros(len(samples)) + FRBDMs=np.zeros(len(samples)) + for i in range(len(samples)): + FRBZs[i]=samples[i][0] + FRBDMs[i]=samples[i][1] + ######################## + for i,g in enumerate(gs): + ################################# + g.state.photo.smearing=True + g.calc_rates() + ################################ if i==0: mean_rates=g.rates * ss[i].TOBS * 10**g.state.FRBdemo.lC else: mean_rates += g.rates * ss[i].TOBS * 10**g.state.FRBdemo.lC time += ss[i].TOBS - plt.figure() ax1 = plt.gca() @@ -76,14 +87,14 @@ def main(): ax2 = plt.gca() # chooses the first arbitrarily to extract zvals etc from + name = names[0] s=ss[0] g=gs[0] - name = names[0] figures.plot_grid(mean_rates,g.zvals,g.dmvals, name=opdir+name+"_zDM.pdf",norm=3,log=True, label='$\\log_{10} p({\\rm DM}_{\\rm IGM} + {\\rm DM}_{\\rm host},z)$ [a.u.]', project=False,ylabel='${\\rm DM}_{\\rm IGM} + {\\rm DM}_{\\rm host}$', - zmax=zmax,DMmax=DMmax,Aconts=[0.01,0.1,0.5]) + FRBZs=FRBZs,FRBDMs=FRBDMs,zmax=zmax,DMmax=DMmax,Aconts=[0.01,0.1,0.5]) pz = np.sum(mean_rates,axis=1) pz /= np.max(pz) From a21640d746dfc4bde64dc26fe3788d5880d7fa60 Mon Sep 17 00:00:00 2001 From: BryceSmith494 Date: Mon, 1 Dec 2025 10:52:07 +0800 Subject: [PATCH 03/35] Fake survey from MC generation --- zdm/MCMC.py | 1 - zdm/grid.py | 26 ++++--- zdm/iteration.py | 1 - zdm/parameters.py | 2 +- .../plot_ASKAP_ICS.py | 26 ++++--- zdm/scripts/create_fake_survey.py | 78 +++++++++++++++++++ 6 files changed, 109 insertions(+), 25 deletions(-) create mode 100644 zdm/scripts/create_fake_survey.py diff --git a/zdm/MCMC.py b/zdm/MCMC.py index 42d00fc5..e5fa30e7 100644 --- a/zdm/MCMC.py +++ b/zdm/MCMC.py @@ -155,7 +155,6 @@ def calc_log_posterior(param_vals, state, params, surveys_sep, Pn=False, pNreps= llsum += ll if ind_surveys: ll_list.append(ll) - #except ValueError as e: # print("Error, setting likelihood to -inf: " + str(e)) # llsum = -np.inf diff --git a/zdm/grid.py b/zdm/grid.py index 5a22e15a..3a21b020 100644 --- a/zdm/grid.py +++ b/zdm/grid.py @@ -497,6 +497,10 @@ def calc_rates(self): self.sfr_smear = np.multiply(self.smear_grid.T, self.sfr).T self.rates = self.pdv * self.sfr_smear + + if self.state.photo.smearing is True: + self.smear_z(self.rates) + self.rates=self.smear_zgrid def get_rates(self): """ @@ -511,9 +515,6 @@ def get_rates(self): rates = rates*self.survey.dm_mask return rates - if self.state.photo.smearing is True: - self.smear_z() - self.rates=self.smear_zgrid def calc_thresholds(self, F0:float, eff_table, @@ -676,7 +677,8 @@ def GenMCSample(self, N, Poisson=False): frb = self.GenMCFRB(Emax_boost) sample.append(frb) - + + sample = np.array(sample) return sample @@ -724,9 +726,14 @@ def initMC(self): pzDM [:,setDMzero] = 0. # weighted pzDM - wb_fraction = (self.beam_o[i] * w * pzDM) + wb_fraction = (self.beam_o[i]* w * pzDM) pdv = np.multiply(wb_fraction.T, self.dV).T rate = pdv * self.sfr_smear + + if self.state.photo.smearing is True: + self.smear_z(rate) + rate=np.copy(self.smear_zgrid) + rates.append(rate) pwb[i * nw + j] = np.sum(rate) @@ -745,7 +752,7 @@ def initMC(self): # saves individal wxb zDM rates for sampling these distributions self.MCrates = rates - + # saves projections onto z-axis self.MCpzcs = pzcs @@ -811,7 +818,6 @@ def GenMCFRB(self, Emax_boost): # get p(z,DM) distribution for this b,w pzDM = self.MCrates[which] - pzc = self.MCpzcs[which] r = np.random.rand(1)[0] @@ -1189,12 +1195,12 @@ def chk_upd_param(self, param: str, vparams: dict, update=False): ######################################## - def smear_z(self): - r,c=self.rates.shape + def smear_z(self,array): + r,c=array.shape smear_size=int(self.state.photo.sigma_width*self.state.photo.sigma*len(self.zvals)/max(self.zvals))+1 smear_arr=random.normal(loc=1,scale=self.state.photo.sigma,size=(smear_size)) smear_arr/=np.sum(smear_arr) if not hasattr(self,"smear_zgrid"): self.smear_zgrid=np.zeros([r,c]) for i in range(c): - self.smear_zgrid[:,i]=np.convolve(self.rates[:,i],smear_arr,mode="same") + self.smear_zgrid[:,i]=np.convolve(array[:,i],smear_arr,mode="same") diff --git a/zdm/iteration.py b/zdm/iteration.py index 39465dda..908ee09e 100644 --- a/zdm/iteration.py +++ b/zdm/iteration.py @@ -90,7 +90,6 @@ def get_log_likelihood(grid, s, norm=True, psnr=True, Pn=False, pNreps=True, pta else: print("Implementation is only completed for nD 1-3.") exit() - return llsum def calc_likelihoods_1D(grid,survey,doplot=False,norm=True,psnr=True, diff --git a/zdm/parameters.py b/zdm/parameters.py index e922cac5..c46784fd 100644 --- a/zdm/parameters.py +++ b/zdm/parameters.py @@ -236,7 +236,7 @@ class WidthParams(data_class.myDataClass): }, ) Wmethod: int = field( - default=3, + default=2, metadata={'help': "Code for width method. 0: ignore it (all 1ms), 1: intrinsic lognormal, 2: include scattering, 3: scat & z-dependence, 4: specific FRB", 'unit': '', 'Notation': '', diff --git a/zdm/scripts/PlotIndividualExperiments/plot_ASKAP_ICS.py b/zdm/scripts/PlotIndividualExperiments/plot_ASKAP_ICS.py index d3bb6a86..2f9d59cb 100644 --- a/zdm/scripts/PlotIndividualExperiments/plot_ASKAP_ICS.py +++ b/zdm/scripts/PlotIndividualExperiments/plot_ASKAP_ICS.py @@ -46,7 +46,7 @@ def main(): # Initialise surveys and grids sdir = os.path.join(resource_filename('zdm', 'data'), 'Surveys') - names=['CRAFT_ICS_892','CRAFT_ICS_1300','CRAFT_ICS_1632'] + names=['CRAFT_ICS_892']#,'CRAFT_ICS_1300','CRAFT_ICS_1632'] state = parameters.State() state.set_astropy_cosmo(Planck18) @@ -61,18 +61,10 @@ def main(): # gets sum of rates over three sets of observations # weights by constant and TOBS time=0 - ########################### - samples=gs[1].GenMCSample(100) - FRBZs=np.zeros(len(samples)) - FRBDMs=np.zeros(len(samples)) - for i in range(len(samples)): - FRBZs[i]=samples[i][0] - FRBDMs[i]=samples[i][1] - ######################## for i,g in enumerate(gs): ################################# - g.state.photo.smearing=True + g.state.photo.smearing=False g.calc_rates() ################################ if i==0: @@ -90,11 +82,21 @@ def main(): name = names[0] s=ss[0] g=gs[0] + ######################## + ''' + samples=gs[1].GenMCSample(100) + FRBZs=np.zeros(len(samples)) + FRBDMs=np.zeros(len(samples)) + for i in range(len(samples)): + FRBZs[i]=samples[i][0] + FRBDMs[i]=samples[i][1] + ''' + ######################## figures.plot_grid(mean_rates,g.zvals,g.dmvals, name=opdir+name+"_zDM.pdf",norm=3,log=True, label='$\\log_{10} p({\\rm DM}_{\\rm IGM} + {\\rm DM}_{\\rm host},z)$ [a.u.]', project=False,ylabel='${\\rm DM}_{\\rm IGM} + {\\rm DM}_{\\rm host}$', - FRBZs=FRBZs,FRBDMs=FRBDMs,zmax=zmax,DMmax=DMmax,Aconts=[0.01,0.1,0.5]) + zmax=zmax,DMmax=DMmax,Aconts=[0.01,0.1,0.5]) pz = np.sum(mean_rates,axis=1) pz /= np.max(pz) @@ -145,7 +147,7 @@ def main(): plt.tight_layout() plt.savefig(opdir+name+"_pdm.pdf") plt.close() - + print(it.get_log_likelihood(g,s)) diff --git a/zdm/scripts/create_fake_survey.py b/zdm/scripts/create_fake_survey.py new file mode 100644 index 00000000..c18e4128 --- /dev/null +++ b/zdm/scripts/create_fake_survey.py @@ -0,0 +1,78 @@ +import os +import statistics + +from astropy.cosmology import Planck18 +from zdm import cosmology as cos +from zdm import figures +from zdm import parameters +from zdm import survey +from zdm import pcosmic +from zdm import iteration as it +from zdm import loading +from zdm import io +from zdm import optical as opt + +import numpy as np +from zdm import survey +from matplotlib import pyplot as plt +from pkg_resources import resource_filename + +def create_fake_survey(smearing=False): + path="/home/brycesmith/Desktop/zdm/zdm/data/Surveys/" + file="Fake_Surveys.ecsv" + + IntroStr="""# %ECSV 1.0 +# --- +# datatype: +# - {name: TNS, datatype: string} +# - {name: DM, datatype: float64} +# - {name: RA, datatype: string} +# - {name: DEC, datatype: string} +# - {name: Z, datatype: float64} +# - {name: SNR, datatype: float64} +# - {name: WIDTH, datatype: float64} +# - {name: Gl, unit: deg, datatype: float64} +# - {name: Gb, unit: deg, datatype: float64} +# - {name: DMG, datatype: float64} +# - {name: FBAR, datatype: float64} +# - {name: BW, datatype: float64} +# meta: !!omap +# - {survey_data: '{"observing": {"NORM_FRB": 17,"TOBS": 64.68,"MAX_IW": 8, "MAXWMETH": 2}, +# "telescope": {"BEAM": "CRACO_900", "DMMASK": "craco_900_mask.npy", +# "DIAM": 12.0, "NBEAMS": 1, "NBINS": 5, "FBAR": 906, +# "TRES": 13.8, "FRES": 1.0, "THRESH": 1.01}}'}\n""" + + param_dict={'sfr_n': 0.21, 'alpha': 0.11, 'lmean': 2.18, 'lsigma': 0.42, 'lEmax': 41.37, + 'lEmin': 39.47, 'gamma': -1.04, 'H0': 70.23, 'halo_method': 0, 'sigmaDMG': 0.0, 'sigmaHalo': 0.0,'lC': -7.61} + state=parameters.State() + state.set_astropy_cosmo(Planck18) + state.update_params(param_dict) + + name=['CRAFT_CRACO_900'] + sdir = os.path.join(resource_filename('zdm', 'data'), 'Surveys') + ss,gs=loading.surveys_and_grids(survey_names=name,repeaters=False,init_state=state,sdir=sdir) + gs=gs[0] + gs.state.photo.smearing=smearing + gs.calc_rates() + samples=gs.GenMCSample(100) + fp=open(path+file,"w+") + fp.write(IntroStr) + fp.write("TNS DM RA DEC Z SNR WIDTH Gl Gb DMG FBAR BW\n") + for i in range(len(samples)): + fp.write('{0:5}'.format(str(i))) + fp.write('{0:20}'.format(str(samples[i][1]+35))) + fp.write('{0:10}'.format("00:00:00")) + fp.write('{0:10}'.format("00:00:00")) + fp.write('{0:25}'.format(str(samples[i][0]))) + fp.write('{0:25}'.format(str(samples[i][3]*10))) + fp.write('{0:8}'.format("31.0")) + fp.write('{0:8}'.format("31.0")) + fp.write('{0:8}'.format("31.0")) + fp.write('{0:8}'.format("35.0")) + fp.write('{0:8}'.format("888")) + fp.write('{0:8}'.format("288")) + fp.write("\n") + fp.close() + + +create_fake_survey(smearing=False) From a27d6eeef2ed1dd4a5f1f15c2fbe5ad69597e782 Mon Sep 17 00:00:00 2001 From: BryceSmith494 Date: Mon, 1 Dec 2025 11:01:39 +0800 Subject: [PATCH 04/35] Fake Surveys --- zdm/data/Surveys/CRAFT_CRACO_900.ecsv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zdm/data/Surveys/CRAFT_CRACO_900.ecsv b/zdm/data/Surveys/CRAFT_CRACO_900.ecsv index 5ad21445..1f260ed1 100644 --- a/zdm/data/Surveys/CRAFT_CRACO_900.ecsv +++ b/zdm/data/Surveys/CRAFT_CRACO_900.ecsv @@ -16,7 +16,7 @@ # - {name: FBAR, datatype: float64} # - {name: BW, datatype: float64} # meta: !!omap -# - {survey_data: '{"observing": {"NORM_FRB": 17,"TOBS": 64.68,"MAX_IW": 8, "MAXWMETH": 2}, +# - {survey_data: '{"observing": {"NORM_FRB": 17,"TOBS": 64.68,"MAX_IDT": 4096,"MAX_IW": 8, "MAXWMETH": 2}, # "telescope": {"BEAM": "CRACO_900", "DMMASK": "craco_900_mask.npy", # "DIAM": 12.0, "NBEAMS": 1, "NBINS": 5, "FBAR": 906, # "TRES": 13.8, "FRES": 1.0, "THRESH": 1.01}}'} From 1c788da43098f31c131d1e0332e8c77725ace740 Mon Sep 17 00:00:00 2001 From: Clancy James Date: Sat, 6 Dec 2025 16:53:16 +0800 Subject: [PATCH 05/35] first few little changes --- zdm/optical.py | 17 ++++++++++++++ zdm/optical_params.py | 53 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/zdm/optical.py b/zdm/optical.py index 26ac15cd..753cd56d 100644 --- a/zdm/optical.py +++ b/zdm/optical.py @@ -822,3 +822,20 @@ def load_marnoch_data(): infile = os.path.join(resources.files('zdm'), 'data', 'optical', datafile) table = Table.read(infile, format='ascii.ecsv') return table + + +############ Routines associated with Nick's model ################ + +class host_model: + """ + This class initiates a model based on ... + """ + + + + def + +def gen_mag_dist(z,f): + """ + generates a magnitude distribution as a function of z and f + """ diff --git a/zdm/optical_params.py b/zdm/optical_params.py index f1eef223..86b9da89 100644 --- a/zdm/optical_params.py +++ b/zdm/optical_params.py @@ -7,6 +7,11 @@ @dataclass class Hosts(data_class.myDataClass): + """ + Data class to hold the generic host galaxy class with no + pre-specified model + + """ # None of the fields should start with an X Absmin: float = field( default=-30, @@ -68,3 +73,51 @@ class Hosts(data_class.myDataClass): 'unit': '', 'Notation': '', }) + +class SFRmodel(data_class.myDataClass): + """ + Data class to hold the SFR model from Nick, which models + FRBs as some fraction of the star-formation rate + """ + fSFR: float = field( + default=0.5, + metadata={'help': "Fraction of FRBs associated with star-formation", + 'unit': '', + 'Notation': '', + }) + NzBins: int = field( + default=10, + metadata={'help': "Number of redshift bins over which the histograms are calculated", + 'unit': '', + 'Notation': '', + }) + zmin: float = field( + default=0., + metadata={'help': "Minimum redshift over which pmag is calculated", + 'unit': '', + 'Notation': '', + }) + zmax: float = field( + default=0., + metadata={'help': "Maximum redshift over which pmag is calculated", + 'unit': '', + 'Notation': '', + }) + NMrBins: int = field( + default=0., + metadata={'help': "Number of magnitude bins", + 'unit': '', + 'Notation': '', + }) + Mrmin: float = field( + default=0., + metadata={'help': "Minimum absolute magnitude over which pmag is calculated", + 'unit': '', + 'Notation': '', + }) + Mrmax: float = field( + default=0., + metadata={'help': "Maximum magnitude over which pmag is calculated", + 'unit': '', + 'Notation': '', + }) From f1b4fee023cd8ae033fa21600bad13e4dedf9b3d Mon Sep 17 00:00:00 2001 From: BryceSmith494 Date: Tue, 16 Dec 2025 13:57:49 +0800 Subject: [PATCH 06/35] Bryce Smith Summer Project 2025 --- zdm/data/Optical/fz_24.7.npy | Bin 0 -> 4928 bytes zdm/data/Optical/zvals.npy | Bin 0 -> 4928 bytes zdm/data/Surveys/Smeared.ecsv | 121 +++++++++ zdm/data/Surveys/Smeared_and_zFrac.ecsv | 106 ++++++++ zdm/data/Surveys/Spectroscopic.ecsv | 121 +++++++++ zdm/data/Surveys/zFrac.ecsv | 106 ++++++++ zdm/grid.py | 29 ++- zdm/parameters.py | 1 - zdm/scripts/H0_scan/run_H0_slice.py | 238 ++++++++++++++++++ zdm/scripts/H0_scan/run_slice.py | 224 +++++++++++++++++ .../plot_ASKAP_CRACO.py | 5 +- .../plot_ASKAP_ICS.py | 6 +- zdm/scripts/create_fake_survey.py | 90 ++++++- zdm/survey_data.py | 5 + 14 files changed, 1038 insertions(+), 14 deletions(-) create mode 100644 zdm/data/Optical/fz_24.7.npy create mode 100644 zdm/data/Optical/zvals.npy create mode 100644 zdm/data/Surveys/Smeared.ecsv create mode 100644 zdm/data/Surveys/Smeared_and_zFrac.ecsv create mode 100644 zdm/data/Surveys/Spectroscopic.ecsv create mode 100644 zdm/data/Surveys/zFrac.ecsv create mode 100644 zdm/scripts/H0_scan/run_H0_slice.py create mode 100644 zdm/scripts/H0_scan/run_slice.py diff --git a/zdm/data/Optical/fz_24.7.npy b/zdm/data/Optical/fz_24.7.npy new file mode 100644 index 0000000000000000000000000000000000000000..ab4c1bbad73719637f120da214449ae844e976b0 GIT binary patch literal 4928 zcmbWr_d6C0pvG~eNs)w75s6CnO5;|cl*%4i*&^#f_TIAh-dkmql~qDkMo3mx_IOeh zBB%FU=Px+-FQ4oFM%-7tC!sChE7W)4_hY$}B=Q&PhXXgL+4v_rcXgB`u?CcO>s~P#f+OxQA{Pz#5 zU;gx;{-@HI|C2@z9NYg46+6wh{?E4$)t8t4S)Q~KIrz`zkr2{~fBux$9d!DqJg?|k z=6^0mb8h7RCBlrW&YS(8jaY=(qBEs};v!bLbB{cxT|}=0>HV^<1vL4R@kazK;0crYp}y-2m>4!KiTO2; z2fpi7VR`dt;@3A!QJY6H{fKQQ+cMV?j4fnO`5dugyT|qMdLeZ?iElQd$2Um-l%*_q`rg2k#q|Hj^7$W1=EdHnn^3N?57 z-93hIr`_c0p0PpL%tW}Siw`2wbk+Mr@c_z*lsiML19)=X?WJbI7qrR-sY=s*fqI%@ zSY2d44wbiCU8Czq^)*XMv-r;-f2}m7bNVy9R=$2I%J0Lw_Bn?Jp*~Faom?t!>_r22 zfZDKfFM>2PWKJ#h;E=G>DQfQ?O#3Z1Gco`BN!Jy%Yd)d<`JA|h`X_AGIfQj?b>oG{ zUWxqVZUkD0Jej@Tjn*qFR_4=PP}%(5z&xLAni@^G6n!t6LGD z+uwhYrxglcw{A*gx4>Av?K(A63(k*ZUc3|EjMr&NURm_bxGlI9a69G$oIdh1*&O=- zF*lu0=E+U)D{}C^cD4!g`j-ZliW*^dHsNZ@wMHz4TlRN$G(eNq(t1q00pDBm=AW#; zN9+jgZt?Kyp=71ln;oxL$q>k(?dI(+hQ2SKE2SrC$fiR?>`{ z-)bQcb(lubs20sC7O~e>YVe(WR9C^J1{M$24+c`zpdv2$f_p|ap1+EVq!X=%-_Mba z*-upn72TU$U|NM?`G?NW4^+XZma|(R?+q9<*1yI}y@7T98*0IYN))NA`S=A_qAR#c zo0`88&o2tNFb`C~Z-L=ZiCYEqY(h?NovXmNd_!KjzH*S$hPG3CmSfmh?x;0?IVw(U z*M&@%p(~?cpIBlUVq-5G+*T-qo>O|B&7m@=u<;J0x0b?A&_}!8uM|8aStc{LOCgX= z5IIUw3eLU<+0>n{k!#LM=@R)G!J`>3 zBX5~Hy~4I>Uq7GZE08JN+$o`c1t#6QT3?1rV8uAbU7u5e9Lgv&8|xB;FZs|PzEJ`? zduE1q>Jns>chr}S6~majm+>rHpF3-EFK$zFsd$bm$g41e${_JhD78;^81+Vrc6F|LMnHh zsq&E#Q{}SIk%t~DlQUOC@<0%jzR50=2ffay#L&ZexJF`3h?f4I1%;j#M!}jaRPQ4cRR&~Xq$_)3 zP9+PYudVmmv1h@@aKlsMS0>2)&!!R@Gtta2DpU}e3GL_iz^s>vf{c)w*Zi6I!Qx3M z-j#{2JVg6a5X%@-X_kq!@T`*ypDG!&n|@+kjJ8Yo{+ zNpMD{VcSbLkjgv_I?G~Tc?fxTYBlD)2YafGPl=mN(Dna z^Xh|~RD1~G_%!L43SsR!A10GjC|rLW`sZFM?9P*>Y4fIHPnc_!(a}^;3#?=={!9U_ z-f+7twTO>89dPr*usB+3<1AU5`1+Nn)cPlV=#pg zc3+UEJtrQAF`rqFrjv2-_)Cm`_B$4q3-Tdvx?^E-eBPS+c`PnB3XBW+#9|wZ9;}aI zF?@@_lzu4|j_L++y)>~XpAC&KS%^V%fmD)5LkxbEH*)zW#oz$7UeF%f7?2Q*PhFOc z!PJn;2QkhV?60LwVb~Xgpe^%An{Uw|HWKz3y^DrM?Ng1!rSvlL30$Sg_H`aGov7@KDOQC76m`iutPUA zqR?}*B4Ceb6v(J&kIvFXVTb5wGw~x5RBJU(pE@G3$aYI{Z+;}4_EG&Y@rr~{gX~^I z^+?F9vMCwxM?!avcXfGRB)F%iBF%>*P(_}!N%b-Uo_<%FZCxVJ6|VMR_+A9mm#$Q6 zorpj|2a`lg7}A_)!sqG2Frz0E9K9Nf4MN55 zBR!$W`7k@eUJ?qPV!FdUVWC(Q_b}MB3X1`T^a;dQiltk*GJ)t_Z6ORE4@CBIOwahYXBcA{)q7I-3?x~B zLq!hHaQ)}FYOLflwA7IQaX<15x_YNP;>H8emGaY0us8r-7RvFV?g0qiHP4tK7XX8& zqzdQR0^l#4z`kqU9}_ONLf6~;5%G4ZVb(5kw2utU^hK7v76-43FT4-(Ev2jYqEIEFCWzk`!;>#TNEx7-Wv7yYsv1H9ln-oN=*-3yvlWYn(QUdU4BH4P^7LgbKA9Q$Wa)Q{HO{Fv_v znQl(qJU35V`=zQ!sp1JEujU>B%;E6px&LC*0eHLwTLfUjf$5wvr!{r3HG_I-d zIO6<6vauiCj;NOSb>()pBM82MQ3+0tSf&4Hw<_m|+h4f)SI#-YKk9bsJ5onXvCMfU z4>&+!Uyb0~Vh6a0o$|co?*PWsHp-)q9FQkQ%Jf#i0ht}HOl$ic@N53To7@q5P+c^? zWKv`g>0IVYNe_E`8E&Bwm$wJs0Euomt3CQ=h8G&Y+d-jS#Y3ak4wt$}bm{`^&{}iF zidxAII|L&xDP}udoP4x)c+M7Q_Ujh>EVRXpm{jO~Yg=TqH3dWo+hQqV(s*#=DQ1-i z7o^`jg}ls3mR9?xIR3WvbEWW84BDknI;`5@O5nj>okAP*UT5p`Hnzd8FP^ECY&Hlz zy_u&oY7MnkDy{N3YYgvpDi2YxMro7`BMX%^K3}#S&~CB9D0hDNsFxM$l9O)!5VJx- zC&L-{Z303bDL%bbMF6wMjU&ew; z=A8R}{;DMog(RmJky_%KKi$RbCJWr4msrsXv;a4=rQrv83+Ri!h&puK0_X2&?$Ug3 z4x=xpUhh^g$KQEh?U*Su@Xzm+`e0{<;+gf3X$mt;T=+=jOEHD8$5(rDK2!MZqWH3_ z;R#raoest;Kf%zInu8}6OrZHwWntRc1m=Q&XZUDLAjvq=XOd-%s5O2jVNqiY3k^7# zbUen7Sv{^FI*&1b@8rYY4I@05^S96rGlC887aOnhMkqZa6c*HI2pbM{wF^3ipxvm< zZQn7#RdxpI*U1Jr#Hf3L_L>1yEhQ93M)hI(?EN=o4}Hj<(3oPB@q)~m<5V4O}2s-@AzkKOIoh1EK6p3*(SZm0u$ z+qVqEG&)G^zRkB(s|~3w`FGJK+V~`T;7T67HnP_1t2Ey~LQL%-AGO6Jq+ehwmOb$Z rcb7hUaz$uimxq@e$9GM5Ki2y;Agzh2`0&)m0u2mpQWN$Z)WH7$d%Qj2 literal 0 HcmV?d00001 diff --git a/zdm/data/Optical/zvals.npy b/zdm/data/Optical/zvals.npy new file mode 100644 index 0000000000000000000000000000000000000000..74dcffd075a96ce379eaa928ae9f50c4fc35e379 GIT binary patch literal 4928 zcmbW)$*W{%83*8+$6HlrKDbF5TyzH-2?!EIJqH)+Ld6IoLO>eRt)Q5;JGw{^++^cI z1>NXEkO;bvAOs-_0X1mEF^Msq(eykIcOLuRYv1~wzrb%-r_T4*`~1$m-&^&=Pk-)H zpZP+w@vX*_haWwD`b(z{fAG-Z51n}E@X8^mJ$n4$`p1ra?ey_` z*H3@t*vaGfULX0ugAX2k;OL=mANs%7=*jGvkDq(u@#?wzp1SeybKk3e^5glcZa#c) zde!>uHHXi&sHz29l7_;Z>rCR0@b-E4hsIR z`o_U+C!enx^Y#1c6Z1FzP#sjcx1pMsJ@Z0!|G~%J_owR1Cl1!1tAlQ`XI`w<5AIs~ zOZAa?1AndNw>|Z@>K}ifuPUG4_|IzI)+7J&`7hb`635H-zoM^K)cH5htLnX~-~Xul znmk@p{|v|L{IB!8A*VO0AIx2E%H=J-x2llu+nn#n>mAS+Xe$T6SUf<`{y`cXK>R;fvD6fk=7v*-5>yrE~@m-SRWzX-jJTLQJk?R%iD;!rj zu5w)ExW;jf;~K|xj_VxPIc{*=;JCqYljA1GO^%x!w>WNb+~TUT-KC4DccyQKdm^_Mu7<+aSSEVpH@W%(`hEz5C*b48vj zyeo2D;a=fb$Q4I>$Q4I>$Q4I>!db2FC`+ z2FC`+2FE7HCda1dxyiA~vB|N;vBj~)vBj~)vBj~?vCXl~vCXl~vCXl~vBR;$vBR;$ zvBR;$ahKyR$6b!Q9CtbHa_n;Ka_n;Ka_n;Ka_n*JaqMyIaqMyIaqMyIbL?~MbL?~M zbL{J@QQtp5pc{3V3ynIQ_l-K7&y70FgGL?B@kSlyOrs9xeWMO@q)~^t(5S=wY1CnU zH0yBQH|sEmnst~9%{t7TW*z27vkvENvkr5nS%-PmtiyO})?t1$>oBL9b(k~FI?TBi zM~kDy(c)-vv^ZKEEshpPi=)NS;%IZUIocd;jy6Y|qs`IgXmhkV+8k|;Hb;k}!_ndB zaCA629374hM~9=s(c$QDbUC^lU5+kCm!r$k<>+#BIl3HOjxI-+qsP(X=yCKodK^8D z9!HO($I;{Har8L)9DR;HN1vn5(dX!M^f~$*eU3gypQFz);23ZWI0hU8jseGjW56-s z7;p?Y1{_0}V~#P$0>=W!0>=W!0>=W!0>=W!0>=W!0>=W! zgk!=n;h1ntI3^qujtR$vW5O}vm~c!urW{j_DaVv!$}#1ba!fg<98-=d$CP8nG2@tV z%s6HoGmaU@jAK^+cK*9!#xdiVam+$r-6;HD&7#oXd!n%ay;11%eNo8cp(ynG(J0jW zWEA@Td=%;)i$ecjjY9ozM#1r&DCG6sD0qGph1`A`1=la5kl(XW@I4=e9AAur^W`Yy z`F2##zyDp1LavKZaBoK(F-Oc1bHp4mN6Zm(#2hh4%n@_M95F}C5p%>GF-Oc1bHp4m zM;xBp<(MPph&d9Dgd^cdI1-M8BjHFm5{`r;;Yc_Vj)WuONH`LXgd^cdI1-M8BjHFm z5{{H3j)J4$C^!m^ zf}`LlI0}w}qu?kw3XX!K;3zl>j*_F~C^<@wlB47(IZBR_qvR+#N{*7Fnewz[i])[0][0] + j1=j2+1 + newfz[i]=fz[j1]+(newz[i]-z[j1])*(fz[j2]-fz[j1])/(z[j2]-z[j1]) + np.save(path+"/"+name+"_fz",newfz) + np.save(path+"/"+name+"_z",newz) + diff --git a/zdm/parameters.py b/zdm/parameters.py index c46784fd..bd7c0dd0 100644 --- a/zdm/parameters.py +++ b/zdm/parameters.py @@ -458,4 +458,3 @@ class PhotometricParams(data_class.myDataClass): sigma_width:int =field(default=6) - diff --git a/zdm/scripts/H0_scan/run_H0_slice.py b/zdm/scripts/H0_scan/run_H0_slice.py new file mode 100644 index 00000000..71597a18 --- /dev/null +++ b/zdm/scripts/H0_scan/run_H0_slice.py @@ -0,0 +1,238 @@ +import argparse +import numpy as np +import os + +from zdm import figures +from zdm import iteration as it + +from zdm import parameters +from zdm import repeat_grid as zdm_repeat_grid +from zdm import MCMC +from zdm import survey +from astropy.cosmology import Planck18 + +from numpy import random +import matplotlib.pyplot as plt +import time + +def main(): + + t0 = time.time() + parser = argparse.ArgumentParser() + #parser.add_argument(dest='param',type=str,help="Parameter to do the slice in") + parser.add_argument(dest='min',type=float,help="Min value") + parser.add_argument(dest='max',type=float,help="Max value") + parser.add_argument('-f', '--files', default=None, nargs='+', type=str, help="Survey file names") + parser.add_argument('-s', '--smeared_surveys', default=None, nargs='+', type=str, help="Surveys with smeared z-vals") + parser.add_argument('-z', '--frac_surveys', default=None, nargs='+', type=str, help="Surveys with z-fraction") + parser.add_argument('-n',dest='n',type=int,default=50,help="Number of values") + # parser.add_argument('-r',dest='repeaters',default=False,action='store_true',help="Surveys are repeater surveys") + args = parser.parse_args() + + vals = np.linspace(args.min, args.max, args.n) + + # Set state + state = parameters.State() + state.set_astropy_cosmo(Planck18) + # param_dict={'sfr_n': 1.13, 'alpha': 1.5, 'lmean': 2.27, 'lsigma': 0.55, + # 'lEmax': 41.26, 'lEmin': 39.5, 'gamma': -0.95, 'H0': 73, + # 'min_lat': 0.0, 'sigmaDMG': 0.0, 'sigmaHalo': 20.0} + # param_dict={'sfr_n': 0.8808527057055584, 'alpha': 0.7895161131856694, + # 'lmean': 2.1198711983468064, 'lsigma': 0.44944780033763343, + # 'lEmax': 41.18671139482926, 'lEmin': 39.81049090314043, 'gamma': -1.1558450520609953, + # 'H0': 54.6887137195215, 'halo_method': 0, 'sigmaDMG': 0.0, 'sigmaHalo': 0.0, 'min_lat': 30.0} + param_dict={'sfr_n': 1.7294049204398037, 'alpha': 1.4859524003747502, + 'lmean': 2.3007428869522486, 'lsigma': 0.396300210604263, + 'lEmax': 41.0, 'lEmin': 38.35533894604933, 'gamma': 0.6032500201815869, + 'H0': 70.51322705185869, 'DMhalo': 39.800465306883666} + # param_dict={'lEmax': 40.578551786703116} + state.update_params(param_dict) + + state.update_param('Rgamma', -2.2) + state.update_param('lRmax', 3.0) + state.update_param('lRmin', -4.0) + #state.update_param('min_lat', 30.0) + + + # Initialise surveys + surveys_sep = [[], []] + s_surveys_sep=[[],[]] + + grid_params = {} + grid_params['dmmax'] = 7000.0 + grid_params['ndm'] = 1400 + grid_params['nz'] = 500 + ddm = grid_params['dmmax'] / grid_params['ndm'] + dmvals = (np.arange(grid_params['ndm']) + 1) * ddm + + if args.files is not None: + for survey_name in args.files: + s = survey.load_survey(survey_name, state, dmvals) + surveys_sep[0].append(s) + t1 = time.time() + print("Step 1: ", str(t1-t0), flush=True) + + # state.update_param('halo_method', 1) + # state.update_param(args.param, vals[0]) + + outdir = 'cube/' + 'H0' + '/' + if not os.path.exists(outdir): + os.makedirs(outdir) + + ll_lists = [] + for val in vals: + print("val:", val) + param = {"H0": {'min': -np.inf, 'max': np.inf}} + ll=0 + ll_list=[] + for i, surveys in enumerate(args.files): + state.photo.smearing=False + surveys_sep[0][i].survey_data.observing.Z_FRACTION=None + if args.smeared_surveys is not None: + if surveys in args.smeared_surveys: + state.photo.smearing=True + if args.frac_surveys is not None: + if surveys in args.frac_surveys: + surveys_sep[0][i].survey_data.observing.Z_FRACTION="lsst_24.7" + sll, sll_list = MCMC.calc_log_posterior([val], state, param,[[surveys_sep[0][i]],[]], grid_params, ind_surveys=True)#,psnr=True) + for s in sll_list: + ll_list.append(s) + ll+=sll + print(ll, ll_list) + ll_lists.append(ll_list) + t2 = time.time() + print("Step 2: ", str(t2-t1), flush=True) + t1 = t2 + print(ll_lists) + ll_lists = np.asarray(ll_lists) + + plt.figure() + linestyles=["-","--",":","-."] + s_names=["None","Photometric","$F_z$","Both"] + plt.clf() + llsum = np.zeros(ll_lists.shape[0]) + surveys = surveys_sep[0] + surveys_sep[1] + FWHM=[] + for i in range(len(surveys)): + s = surveys[i] + lls = ll_lists[:, i] + + lls[lls < -1e10] = -np.inf + lls[np.argwhere(np.isnan(lls))] = -np.inf + + llsum += lls + + lls = lls - np.max(lls) + lls=10**lls + index1=np.where(lls>=0.5)[0][0] + index2=np.where(lls>=0.5)[0][-1] + root1=vals[index1-1]-(0.5-lls[index1-1])*(vals[index1]-vals[index1-1])/(lls[index1]-lls[index1-1]) + root2=vals[index2]-(0.5-lls[index2])*(vals[index2+1]-vals[index2])/(lls[index2+1]-lls[index2]) + FWHM.append(root2-root1) + # plt.figure() + # plt.clf() + plt.plot(vals, lls, label=s_names[i],ls=linestyles[i]) + plt.xlabel('$H_0 (km s^{-1} Mpc^{-1})$',fontsize=14) + plt.ylabel('$\\frac{\\mathcal{L}}{max(\\mathcal{L})}$',fontsize=14) + # plt.savefig(os.path.join(outdir, s.name + ".pdf")) + print("Max H0:",vals[np.where(lls==1.0)[0][0]]) + + plt.minorticks_on() + plt.tick_params(axis='y', which='major', labelsize=14) # To set tick label fontsize + plt.tick_params(axis='y', which='major', length=9) # To set tick size + plt.tick_params(axis='y', which='minor', length=4.5) # To set tick size + plt.tick_params(axis='y', which='both',direction='in',right='on', top='on') + + plt.tick_params(axis='x', which='major', labelsize=14) # To set tick label fontsize + plt.tick_params(axis='x', which='major', length=9) # To set tick size + plt.tick_params(axis='x', which='minor', length=4.5) # To set tick size + plt.tick_params(axis='x', which='both',direction='in',right='on', top='on') + + print(vals) + print(llsum) + peak=vals[np.argwhere(llsum == np.max(llsum))[0]] + print("peak", peak,) + #plt.axvline(peak,ls='--') + plt.legend(fontsize=14,loc='upper left') + plt.savefig(outdir + "H0" + ".pdf") + percentage=(FWHM/FWHM[0]-1)*100 + print("FWHM:Spectroscopic,Photometric,zFrac,Photometric+zfrac\n",FWHM,percentage) + + # llsum = llsum - np.max(llsum) + # llsum[llsum < -1e10] = -np.inf + plt.figure() + plt.clf() + plt.plot(vals, llsum, label='Total') + plt.axvline(peak,ls='--') + # plt.plot(vals, llsum2) + plt.xlabel('H0') + plt.ylabel('log likelihood') + plt.legend() + plt.savefig(outdir + "H0"+ "_sum.pdf") + +#============================================================================== +""" +Function: plot_grids +Date: 10/01/2024 +Purpose: + Plot grids. Adapted from zdm/scripts/plot_pzdm_grid.py + +Imports: + grids = list of grids + surveys = list of surveys + outdir = output directory + val = parameter value for this grid +""" +def plot_grids(grids, surveys, outdir, val): + for g,s in zip(grids, surveys): + zvals=[] + dmvals=[] + nozlist=[] + + if s.zlist is not None: + for iFRB in s.zlist: + zvals.append(s.Zs[iFRB]) + dmvals.append(s.DMEGs[iFRB]) + if s.nozlist is not None: + for dm in s.DMEGs[s.nozlist]: + nozlist.append(dm) + + frbzvals = np.array(zvals) + frbdmvals = np.array(dmvals) + + figures.plot_grid( + g.rates, + g.zvals, + g.dmvals, + name=outdir + s.name + "_" + str(val) + ".pdf", + norm=3, + log=True, + label="$\\log_{10} p({\\rm DM}_{\\rm EG},z)$ [a.u.]", + project=False, + FRBDM=frbdmvals, + FRBZ=frbzvals, + Aconts=[0.01, 0.1, 0.5], + zmax=1.5, + DMmax=3000, + # DMlines=nozlist, + ) + +#============================================================================== +""" +Function: commasep +Date: 23/08/2022 +Purpose: + Turn a string of variables seperated by commas into a list + +Imports: + s = String of variables + +Exports: + List conversion of s +""" +def commasep(s): + return list(map(str, s.split(','))) + +#============================================================================== + +main() diff --git a/zdm/scripts/H0_scan/run_slice.py b/zdm/scripts/H0_scan/run_slice.py new file mode 100644 index 00000000..8c51482d --- /dev/null +++ b/zdm/scripts/H0_scan/run_slice.py @@ -0,0 +1,224 @@ +import argparse +import numpy as np +import os + +from zdm import figures +from zdm import iteration as it + +from zdm import parameters +from zdm import repeat_grid as zdm_repeat_grid +from zdm import MCMC +from zdm import survey +from astropy.cosmology import Planck18 + +from numpy import random +import matplotlib.pyplot as plt +import time + +def main(): + + t0 = time.time() + parser = argparse.ArgumentParser() + parser.add_argument(dest='param',type=str,help="Parameter to do the slice in") + parser.add_argument(dest='min',type=float,help="Min value") + parser.add_argument(dest='max',type=float,help="Max value") + parser.add_argument('-f', '--files', default=None, nargs='+', type=str, help="Survey file names") + parser.add_argument('-s', '--smeared_surveys', default=None, nargs='+', type=str, help="Surveys with smeared z-vals") + parser.add_argument('-n',dest='n',type=int,default=50,help="Number of values") + # parser.add_argument('-r',dest='repeaters',default=False,action='store_true',help="Surveys are repeater surveys") + args = parser.parse_args() + + vals = np.linspace(args.min, args.max, args.n) + + # Set state + state = parameters.State() + state.set_astropy_cosmo(Planck18) + # param_dict={'sfr_n': 1.13, 'alpha': 1.5, 'lmean': 2.27, 'lsigma': 0.55, + # 'lEmax': 41.26, 'lEmin': 39.5, 'gamma': -0.95, 'H0': 73, + # 'min_lat': 0.0, 'sigmaDMG': 0.0, 'sigmaHalo': 20.0} + # param_dict={'sfr_n': 0.8808527057055584, 'alpha': 0.7895161131856694, + # 'lmean': 2.1198711983468064, 'lsigma': 0.44944780033763343, + # 'lEmax': 41.18671139482926, 'lEmin': 39.81049090314043, 'gamma': -1.1558450520609953, + # 'H0': 54.6887137195215, 'halo_method': 0, 'sigmaDMG': 0.0, 'sigmaHalo': 0.0, 'min_lat': 30.0} + param_dict={'sfr_n': 1.7294049204398037, 'alpha': 1.4859524003747502, + 'lmean': 2.3007428869522486, 'lsigma': 0.396300210604263, + 'lEmax': 41.0, 'lEmin': 38.35533894604933, 'gamma': 0.6032500201815869, + 'H0': 70.51322705185869, 'DMhalo': 39.800465306883666} + # param_dict={'lEmax': 40.578551786703116} + state.update_params(param_dict) + + state.update_param('Rgamma', -2.2) + state.update_param('lRmax', 3.0) + state.update_param('lRmin', -4.0) + #state.update_param('min_lat', 30.0) + #smeared state##################### + s_state=parameters.State() + s_state.set_astropy_cosmo(Planck18) + s_state.update_params(param_dict) + s_state.update_param('Rgamma',-2.2) + s_state.update_param('lRmax', 3.0) + s_state.update_param('lRmin', -4.0) + #################################3 + + + # Initialise surveys + surveys_sep = [[], []] + s_surveys_sep=[[],[]] + + grid_params = {} + grid_params['dmmax'] = 7000.0 + grid_params['ndm'] = 1400 + grid_params['nz'] = 500 + ddm = grid_params['dmmax'] / grid_params['ndm'] + dmvals = (np.arange(grid_params['ndm']) + 1) * ddm + + if args.files is not None: + for survey_name in args.files: + s = survey.load_survey(survey_name, state, dmvals) + surveys_sep[0].append(s) + + if args.smeared_surveys is not None: + for survey_name in args.smeared_surveys: + s_state.photo.smearing=True + s_state.photo.sigma=float(survey_name) + s = survey.load_survey(survey_name, s_state, dmvals) + s_surveys_sep[0].append(s) + t1 = time.time() + print("Step 1: ", str(t1-t0), flush=True) + + # state.update_param('halo_method', 1) + # state.update_param(args.param, vals[0]) + + outdir = 'cube/' + args.param + '/' + if not os.path.exists(outdir): + os.makedirs(outdir) + + ll_lists = [] + for val in vals: + print("val:", val) + param = {args.param: {'min': -np.inf, 'max': np.inf}} + ll, ll_list = MCMC.calc_log_posterior([val], state, param, surveys_sep, grid_params, ind_surveys=True)#,psnr=True) + for i, surveys in enumerate(args.smeared_surveys): + s_state.photo.sigma=float(surveys) + ss_surveys_sep=[[],[]] + ss_surveys_sep[0].append(s_surveys_sep[0][i]) + sll, sll_list = MCMC.calc_log_posterior([val], s_state, param, ss_surveys_sep, grid_params, ind_surveys=True)#,psnr=True) + for s in sll_list: + ll_list.append(s) + ll+=sll + print(ll, ll_list) + ll_lists.append(ll_list) + t2 = time.time() + print("Step 2: ", str(t2-t1), flush=True) + t1 = t2 + print(ll_lists) + ll_lists = np.asarray(ll_lists) + + plt.figure() + plt.clf() + llsum = np.zeros(ll_lists.shape[0]) + surveys = surveys_sep[0] + surveys_sep[1]+s_surveys_sep[0] + for i in range(len(surveys)): + s = surveys[i] + lls = ll_lists[:, i] + + lls[lls < -1e10] = -np.inf + lls[np.argwhere(np.isnan(lls))] = -np.inf + + llsum += lls + + lls = lls - np.max(lls) + #lls=10**lls + # plt.figure() + # plt.clf() + plt.plot(vals, lls, label=s.name) + plt.xlabel(args.param) + plt.ylabel('log likelihood') + # plt.savefig(os.path.join(outdir, s.name + ".pdf")) + + print(vals) + print(llsum) + peak=vals[np.argwhere(llsum == np.max(llsum))[0]] + print("peak", peak) + #plt.axvline(peak,ls='--') + plt.legend() + plt.savefig(outdir + args.param + ".pdf") + + # llsum = llsum - np.max(llsum) + # llsum[llsum < -1e10] = -np.inf + plt.figure() + plt.clf() + plt.plot(vals, llsum, label='Total') + plt.axvline(peak,ls='--') + # plt.plot(vals, llsum2) + plt.xlabel(args.param) + plt.ylabel('log likelihood') + plt.legend() + plt.savefig(outdir + args.param + "_sum.pdf") + +#============================================================================== +""" +Function: plot_grids +Date: 10/01/2024 +Purpose: + Plot grids. Adapted from zdm/scripts/plot_pzdm_grid.py + +Imports: + grids = list of grids + surveys = list of surveys + outdir = output directory + val = parameter value for this grid +""" +def plot_grids(grids, surveys, outdir, val): + for g,s in zip(grids, surveys): + zvals=[] + dmvals=[] + nozlist=[] + + if s.zlist is not None: + for iFRB in s.zlist: + zvals.append(s.Zs[iFRB]) + dmvals.append(s.DMEGs[iFRB]) + if s.nozlist is not None: + for dm in s.DMEGs[s.nozlist]: + nozlist.append(dm) + + frbzvals = np.array(zvals) + frbdmvals = np.array(dmvals) + + figures.plot_grid( + g.rates, + g.zvals, + g.dmvals, + name=outdir + s.name + "_" + str(val) + ".pdf", + norm=3, + log=True, + label="$\\log_{10} p({\\rm DM}_{\\rm EG},z)$ [a.u.]", + project=False, + FRBDM=frbdmvals, + FRBZ=frbzvals, + Aconts=[0.01, 0.1, 0.5], + zmax=1.5, + DMmax=3000, + # DMlines=nozlist, + ) + +#============================================================================== +""" +Function: commasep +Date: 23/08/2022 +Purpose: + Turn a string of variables seperated by commas into a list + +Imports: + s = String of variables + +Exports: + List conversion of s +""" +def commasep(s): + return list(map(str, s.split(','))) + +#============================================================================== + +main() diff --git a/zdm/scripts/PlotIndividualExperiments/plot_ASKAP_CRACO.py b/zdm/scripts/PlotIndividualExperiments/plot_ASKAP_CRACO.py index edaf2609..f4d8a67d 100644 --- a/zdm/scripts/PlotIndividualExperiments/plot_ASKAP_CRACO.py +++ b/zdm/scripts/PlotIndividualExperiments/plot_ASKAP_CRACO.py @@ -40,7 +40,7 @@ def main(): # Initialise surveys and grids sdir = resources.files('zdm').joinpath('data/Surveys') names=['CRAFT_CRACO_1300','CRAFT_CRACO_900'] - + ss,gs = loading.surveys_and_grids( survey_names=names,repeaters=False,init_state=state,sdir=sdir) # should be equal to actual number of FRBs, but for this purpose it doesn't matter @@ -68,6 +68,9 @@ def main(): zmax=zmax,DMmax=DMmax,Aconts=[0.01,0.1,0.5], FRBDMs=s.frbs['DMEG'].values,FRBZs=s.frbs['Z'].values, DMlines = s.frbs['DMEG'].values[noz]) + + + exit() pz = np.sum(mean_rates,axis=1) diff --git a/zdm/scripts/PlotIndividualExperiments/plot_ASKAP_ICS.py b/zdm/scripts/PlotIndividualExperiments/plot_ASKAP_ICS.py index 2f9d59cb..fc53936e 100644 --- a/zdm/scripts/PlotIndividualExperiments/plot_ASKAP_ICS.py +++ b/zdm/scripts/PlotIndividualExperiments/plot_ASKAP_ICS.py @@ -64,8 +64,9 @@ def main(): for i,g in enumerate(gs): ################################# - g.state.photo.smearing=False - g.calc_rates() + #g.state.photo.smearing=True + #g.survey.survey_data.observing.Z_FRACTION="lsst_24.7" + #g.calc_rates() ################################ if i==0: mean_rates=g.rates * ss[i].TOBS * 10**g.state.FRBdemo.lC @@ -147,7 +148,6 @@ def main(): plt.tight_layout() plt.savefig(opdir+name+"_pdm.pdf") plt.close() - print(it.get_log_likelihood(g,s)) diff --git a/zdm/scripts/create_fake_survey.py b/zdm/scripts/create_fake_survey.py index c18e4128..8cb09ccf 100644 --- a/zdm/scripts/create_fake_survey.py +++ b/zdm/scripts/create_fake_survey.py @@ -12,14 +12,16 @@ from zdm import io from zdm import optical as opt +from matplotlib import pyplot as plt +from numpy import random import numpy as np from zdm import survey from matplotlib import pyplot as plt from pkg_resources import resource_filename def create_fake_survey(smearing=False): - path="/home/brycesmith/Desktop/zdm/zdm/data/Surveys/" - file="Fake_Surveys.ecsv" + path = os.path.join(resource_filename('zdm', 'data'), 'Surveys')+"/" + file="Spectroscopic.ecsv" IntroStr="""# %ECSV 1.0 # --- @@ -52,9 +54,10 @@ def create_fake_survey(smearing=False): sdir = os.path.join(resource_filename('zdm', 'data'), 'Surveys') ss,gs=loading.surveys_and_grids(survey_names=name,repeaters=False,init_state=state,sdir=sdir) gs=gs[0] - gs.state.photo.smearing=smearing + #gs.state.photo.smearing=smearing gs.calc_rates() samples=gs.GenMCSample(100) + zvals=np.zeros(len(samples)) fp=open(path+file,"w+") fp.write(IntroStr) fp.write("TNS DM RA DEC Z SNR WIDTH Gl Gb DMG FBAR BW\n") @@ -65,14 +68,87 @@ def create_fake_survey(smearing=False): fp.write('{0:10}'.format("00:00:00")) fp.write('{0:25}'.format(str(samples[i][0]))) fp.write('{0:25}'.format(str(samples[i][3]*10))) - fp.write('{0:8}'.format("31.0")) - fp.write('{0:8}'.format("31.0")) - fp.write('{0:8}'.format("31.0")) + fp.write('{0:8}'.format("-1.0")) + fp.write('{0:8}'.format("-1.0")) + fp.write('{0:8}'.format("-1.0")) fp.write('{0:8}'.format("35.0")) fp.write('{0:8}'.format("888")) fp.write('{0:8}'.format("288")) fp.write("\n") fp.close() + if smearing is True: + sigmas=np.array([0.035]) + for sigma in sigmas: + for i in range(len(samples)): + zvals[i]=samples[i][0] + file="Smeared.ecsv" + fp=open(path+file,"w+") + fp.write(IntroStr) + fp.write("TNS DM RA DEC Z SNR WIDTH Gl Gb DMG FBAR BW\n") + smear_error=random.normal(loc=0,scale=sigma,size=100) + newvals=zvals+smear_error + for i in range(len(samples)): + fp.write('{0:5}'.format(str(i))) + fp.write('{0:20}'.format(str(samples[i][1]+35))) + fp.write('{0:10}'.format("00:00:00")) + fp.write('{0:10}'.format("00:00:00")) + fp.write('{0:25}'.format(str(newvals[i]))) + fp.write('{0:25}'.format(str(samples[i][3]*10))) + fp.write('{0:8}'.format("-1.0")) + fp.write('{0:8}'.format("-1.0")) + fp.write('{0:8}'.format("-1.0")) + fp.write('{0:8}'.format("35.0")) + fp.write('{0:8}'.format("888")) + fp.write('{0:8}'.format("288")) + fp.write("\n") + fp.close() + + frac_path=path+"/../Optical/" + fz=np.load(frac_path+"lsst_24.7_fz.npy")[0:500] + zs=np.load(frac_path+"lsst_24.7_z.npy")[0:500] + file="zFrac.ecsv" + fp=open(path+file,"w+") + fp1=open(path+"Smeared_and_zFrac.ecsv","w+") + fp.write(IntroStr) + fp1.write(IntroStr) + fp.write("TNS DM RA DEC Z SNR WIDTH Gl Gb DMG FBAR BW\n") + fp1.write("TNS DM RA DEC Z SNR WIDTH Gl Gb DMG FBAR BW\n") + for i in range(len(samples)): + prob_thresh=random.rand() + j=np.where(zs>samples[i][0]-0.005)[0][0] + prob=fz[j] + if prob>=prob_thresh: + fp.write('{0:5}'.format(str(i))) + fp.write('{0:20}'.format(str(samples[i][1]+35))) + fp.write('{0:10}'.format("00:00:00")) + fp.write('{0:10}'.format("00:00:00")) + fp.write('{0:25}'.format(str(samples[i][0]))) + fp.write('{0:25}'.format(str(samples[i][3]*10))) + fp.write('{0:8}'.format("-1.0")) + fp.write('{0:8}'.format("-1.0")) + fp.write('{0:8}'.format("-1.0")) + fp.write('{0:8}'.format("35.0")) + fp.write('{0:8}'.format("888")) + fp.write('{0:8}'.format("288")) + fp.write("\n") + + fp1.write('{0:5}'.format(str(i))) + fp1.write('{0:20}'.format(str(samples[i][1]+35))) + fp1.write('{0:10}'.format("00:00:00")) + fp1.write('{0:10}'.format("00:00:00")) + fp1.write('{0:25}'.format(str(samples[i][0]+smear_error[i]))) + fp1.write('{0:25}'.format(str(samples[i][3]*10))) + fp1.write('{0:8}'.format("-1.0")) + fp1.write('{0:8}'.format("-1.0")) + fp1.write('{0:8}'.format("-1.0")) + fp1.write('{0:8}'.format("35.0")) + fp1.write('{0:8}'.format("888")) + fp1.write('{0:8}'.format("288")) + fp1.write("\n") + + fp.close() + fp1.close() + -create_fake_survey(smearing=False) +create_fake_survey(True) diff --git a/zdm/survey_data.py b/zdm/survey_data.py index 35ff2b11..73399c7e 100644 --- a/zdm/survey_data.py +++ b/zdm/survey_data.py @@ -300,6 +300,11 @@ class Observing(data_class.myDataClass): 'Notation': '', }) + Z_FRACTION:str =field( + default=None, + metadata={'help':"Fraction of visible FRBs at a redshift"} + ) + class SurveyData(data_class.myData): """ Hold the SurveyData in a convenient object From d6df03a30d0b3f6261e1ecc49b2900d18cec937d Mon Sep 17 00:00:00 2001 From: Clancy James Date: Wed, 31 Dec 2025 15:52:31 +0800 Subject: [PATCH 07/35] finished script to load in Nicks optical module and compute p mr distributions --- ...and_probabilities_vlt-fors2_R-SPECIAL.ecsv | 2143 +++++++++++++++++ .../p_mr_distributions_dz0.01_z_in_0_1.2.h5 | Bin 0 -> 467784 bytes zdm/loading.py | 2 +- zdm/misc_functions.py | 2 +- zdm/optical.py | 824 +++++-- zdm/optical_params.py | 88 +- zdm/parameters.py | 6 +- zdm/scripts/Path/plot_host_models.py | 297 +++ .../plot_Meertrap.py | 4 +- 9 files changed, 3105 insertions(+), 261 deletions(-) create mode 100644 zdm/data/optical/magnitudes_and_probabilities_vlt-fors2_R-SPECIAL.ecsv create mode 100644 zdm/data/optical/p_mr_distributions_dz0.01_z_in_0_1.2.h5 create mode 100644 zdm/scripts/Path/plot_host_models.py diff --git a/zdm/data/optical/magnitudes_and_probabilities_vlt-fors2_R-SPECIAL.ecsv b/zdm/data/optical/magnitudes_and_probabilities_vlt-fors2_R-SPECIAL.ecsv new file mode 100644 index 00000000..eadfd47a --- /dev/null +++ b/zdm/data/optical/magnitudes_and_probabilities_vlt-fors2_R-SPECIAL.ecsv @@ -0,0 +1,2143 @@ +# %ECSV 1.0 +# --- +# datatype: +# - {name: FRB20180301A, datatype: float64} +# - {name: FRB20180916B, datatype: float64} +# - {name: FRB20190520B, datatype: float64} +# - {name: FRB20201124A, datatype: float64} +# - {name: FRB20210410D, datatype: float64} +# - {name: FRB20121102A, datatype: float64} +# - {name: FRB20180924B, datatype: float64} +# - {name: FRB20181112A, datatype: float64} +# - {name: FRB20190102C, datatype: float64} +# - {name: FRB20190608B, datatype: float64} +# - {name: FRB20190611B, datatype: float64} +# - {name: FRB20190711A, datatype: float64} +# - {name: FRB20190714A, datatype: float64} +# - {name: FRB20191001A, datatype: float64} +# - {name: FRB20200430A, datatype: float64} +# - {name: FRB20200906A, datatype: float64} +# - {name: FRB20210117A, datatype: float64} +# - {name: FRB20210320C, datatype: float64} +# - {name: FRB20210807D, datatype: float64} +# - {name: FRB20211127I, datatype: float64} +# - {name: FRB20211203C, datatype: float64} +# - {name: FRB20211212A, datatype: float64} +# - {name: FRB20220105A, datatype: float64} +# - {name: z, datatype: float64} +# - {name: n>lim, datatype: int64} +# - {name: nlim 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

7<>tV-$Bpp2oS%K->vp%oT#E3pa>j!( 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 literal 0 HcmV?d00001 diff --git a/zdm/loading.py b/zdm/loading.py index c641046c..59d9a97d 100644 --- a/zdm/loading.py +++ b/zdm/loading.py @@ -230,7 +230,7 @@ def surveys_and_grids(init_state=None, alpha_method=1, cos.set_cosmology(state) cos.init_dist_measures() - # get the grid of p(DM|z) + # get the grid of p(DMcosmic|z) zDMgrid, zvals,dmvals = misc_functions.get_zdm_grid( state, new=True, plot=False, method='analytic', nz=nz, ndm=ndm, zmax=zmax, dmmax=dmmax, diff --git a/zdm/misc_functions.py b/zdm/misc_functions.py index c0042e59..f11a913c 100644 --- a/zdm/misc_functions.py +++ b/zdm/misc_functions.py @@ -1524,7 +1524,7 @@ def initialise_grids( # generates a DM mask # creates a mask of values in DM space to convolve with the DM grid mask = pcosmic.get_dm_mask( - dmvals, (state.host.lmean, state.host.lsigma), zvals, plot=True + dmvals, (state.host.lmean, state.host.lsigma), zvals, plot=False ) grids = [] for survey in surveys: diff --git a/zdm/optical.py b/zdm/optical.py index 753cd56d..e9a2acfa 100644 --- a/zdm/optical.py +++ b/zdm/optical.py @@ -3,10 +3,45 @@ the FRB/astropath module and (optical) FRB host galaxy information. -It includes the class "host_model" for describing the -intrinsic FRB host galaxy distribution, associated functions, -and the approximate fraction of -detectable FRBs from Marnoch et al (https://doi.org/10.1093/mnras/stad2353) +The philosophy of the module is this. The base class +is "host_model". This class is the top-level class +that contains base functions to e.g. calculate +p(m_r|DM). + +However, no host_model class contains any astroiphysics. + +Instead, it wraps an underlying set of possible class +objects that must have a specific set of callable functions +which each contain the relevant calculations. + +The current set are the following: + +Simple_host_model: + Describes intrinsic host properties as a spline + interpolation between p(M_r) described by N + points. N parameters (e.g. 10). + +Marnoch_model: + Fixed calculation of p(M_r) based on extrapolation + of known FRB host galaxies. No parameters. See + https://doi.org/10.1093/mnras/stad2353 + + +Loudas_model: + Calculates p(M_r) via assigning a fraction of FRB + hosts to follow star-formation in galaxies, and + a fraction to stellar mass, then includes the modelled + evolution of these galaxies. 1 parameter. + +Each "host_model" class object above must provide functions to: + __init__ + calculate p(m_r|z,parameters) + +The wrapper class provides the following fubctions: + +- init_path_raw_prior_Oi(self,DM,grid): + (takes as input an FRB DM, and grid object) + """ @@ -15,24 +50,301 @@ from zdm import cosmology as cos from zdm import optical_params as op from scipy.interpolate import CubicSpline +from scipy.interpolate import make_interp_spline import os from importlib import resources import pandas +import h5py + +################################################################### +############ Routines associated with Nick's model ################ +################################################################### -class host_model: +class loudas_model: + """ + This class initiates a model based on Nick Loudas's model of + galaxy magnitudes as a function of redshift. The underlying + model is a description of galaxies as a function of + stellar mass and star-formation rate as a function of redshift. + + """ + + def __init__(self,OpticalState=None,fname='p_mr_distributions_dz0.01_z_in_0_1.2.h5',data_dir=None,verbose=False): + """ + initialises the model. Loads data provided by Nick Loudas + on mass- and sfr-weighted magnitudes. + + Args: + fname [string]: h55 filename containing the data + datadir [string]: directory that the data is contained in. Defaults to None. + """ + + # uses the "simple hosts" descriptor + if OpticalState is None: + OpticalState = op.OpticalState() + self.OpticalState = OpticalState + + #extract the correct optical substate from the opstate + self.opstate = self.OpticalState.loudas + + self.fsfr = self.opstate.fSFR + + + # checks that cosmology is initialised + if not cos.INIT: + cos.init_dist_measures() + + # gets base input directory. In future, this may be expanded + if data_dir is None: + data_dir = os.path.join(resources.files('zdm'), 'data', 'optical') + + # load data and its properties + self.init_pmr(fname,data_dir) + + # initialises cubic splines for faster speedups + self.init_cubics() + + def init_pmr(self,fname,data_dir): + """ + Loads p(mr|z) distributions from Nick Loudas. Note - these are + actually distributions in apparent magnitude mr. + + Mostly, this wraps around Nick's code "load_p_mr_distributions". + I've kept them separate to distinguish between his code and mine -CWJ. + + """ + ####### loading p(mr) distributions ########## + zbins, rmag_centres, p_mr_sfr, p_mr_mass = self.load_p_mr_distributions( + data_dir, fname = fname) + + # zbins represent ranges. We also calculate z-bin centres + self.zbins = zbins + self.nzbins = zbins.size-1 + self.czbins = 0.5*(self.zbins[1:] + self.zbins[:-1]) + self.logzbins = np.log10(zbins) + self.clogzbins = 0.5*(self.logzbins[1:] + self.logzbins[:-1]) + self.rmags = rmag_centres # centres of rmag bins + self.p_mr_sfr = p_mr_sfr # sfr-weighted p_mr + self.p_mr_mass = p_mr_mass # mass-weighted p_mr + + + # we have now all the data we need! + + def init_cubics(self): + """ + initialises cubic splines that interpolate in mr. For later use (speedup!) + """ + + sfr_splines = [] + mass_splines = [] + for i in np.arange(self.nzbins): + sfr_spline = make_interp_spline(self.rmags,self.p_mr_sfr[i],k=1) + sfr_splines.append(sfr_spline) + + mass_spline = make_interp_spline(self.rmags,self.p_mr_mass[i],k=1) + mass_splines.append(mass_spline) + + self.mass_splines = mass_splines + self.sfr_splines = sfr_splines + + def get_pmr_gz(self,mrbins,z): # fsfr must be a self value z: float,fsfr: float): + """ + Returns the p_mr distribution for a given redshift z and sfr fraction f_sfr + + Args: + z (float): redshift + fsfr (float): fraction of population associated with star-formation + """ + + fsfr = self.fsfr + + # gets interpolation coefficients + lz = np.log10(z) + if lz < self.clogzbins[0]: + # sets values equal to that of smallest bin, to avoid interpolation + i1=0 + i2=1 + k1=1. + k2=0. + elif lz > self.clogzbins[-1]: + i1=self.nzbins-2 + i2=self.nzbins-1 + k1=0 + k2=1. + else: + i1 = np.where(lz > self.clogzbins)[0][-1] # gets lowest value where zs are larger + i2=i1+1 + k2 = (lz-self.clogzbins[i1])/(self.clogzbins[i2]-self.clogzbins[i1]) + k1 = 1.-k2 + + z1=self.czbins[i1] + z2=self.czbins[i2] + + # the mr distributions are apparent magnitudes + # hence, we have to interpolate between z-bins using first-order shifting + # this is *very* important for low values of z + + DL = cos.dl(z) + DL1 = cos.dl(z1) + DL2 = cos.dl(z2) + + # calculates shifts in logarithm. Still shifts when z is lower or higher than m_r + # note: a factor of 2 in DL means a factor of 4 in luminosity, meaning + # 5/2 log10(4) in mr = 5 log10(2). + dmr1 = 5.*np.log10(DL/DL1) # will be a positive shift + dmr2 = 5.*np.log10(DL/DL2) # will be a negative shift + + + mr_centres = (mrbins[:-1]+mrbins[1:])/2. + + # will interpolate the values at *lower* magnitudes, effectively shifting distribution up + p_mr_mass1 = self.mass_splines[i1](mr_centres - dmr1) + + # will interpolate the values at *higher* magnitudes, effectively shifting distribution down + p_mr_mass2 = self.mass_splines[i2](mr_centres - dmr2) + + + # will interpolate the values at *lower* magnitudes, effectively shifting distribution up + p_mr_sfr1 = self.sfr_splines[i1](mr_centres - dmr1) + # will interpolate the values at *higher* magnitudes, effectively shifting distribution down + p_mr_sfr2 = self.sfr_splines[i2](mr_centres - dmr2) + + # distribution for that redshift assuming mass weighting + pmass = k1*p_mr_mass1 + k2*p_mr_mass2 + + # just left here for testing purposes + if False: + print("Redshift bins are ",z,z1,z2) + print("Luminosity distances are ",DL,DL1,DL2) + print("shifts are therefore ",dmr1,dmr2) + + # generate an example plot showing interpolation + plt.plot(self.rmags,p_mr_mass1,linestyle="-",label="scaled from z0") + plt.plot(self.rmags,p_mr_mass2,linestyle="--",label="scaled from z1") + plt.plot(self.rmags,self.p_mr_mass[i1],linestyle=":",label="z0") + plt.plot(self.rmags,self.p_mr_mass[i2],linestyle=":",label="z1") + plt.legend() + plt.tight_layout() + plt.show() + exit() + + # distribution foe that redshift assuming sfr weighting + psfr = k1*p_mr_sfr1 + k2*p_mr_sfr2 + + # mean weighted distribution + pmr = pmass*(1.-fsfr) + psfr*fsfr + + return pmr + + def load_p_mr_distributions(self,data_dir,fname: str = 'p_mr_distributions_dz0.01_z_in_0_1.2.h5') -> tuple: + """ + This code originally written by Nick Loudas. Used with permission + + Load the p(mr|z) distributions from an HDF5 file. + Args: + fname (str): Input filename. + output_dir (str): Directory where the file is stored. Optional (otherwise defaults as below) + Returns: + zbins (np.array): Redshift bin edges. + rmag_centers (np.array): Centers of r-band magnitude bins. + p_mr_sfr (np.array): p(mr|z) for SFR-weighted population. Shape: (len(zbins) - 1, + rmag_resolution). rmag_resolution(=len(rmag_centers)) is fixed across redshift bins. + p_mr_mass (np.array): p(mr|z) for Mass-weighted population. Shape: (len(zbins) - 1, + rmag_resolution). rmag_resolution(=len(rmag_centers)) is fixed across redshift bins. + Note: + The PDF in m_r within a given redshift bin [z1,z2] has been computed at the right edge of the bin (z = z2). + """ + infile = os.path.join(data_dir,fname) + with h5py.File(infile, 'r') as hf: + zbins = np.array(hf['zbins']) + zbins = zbins[1:] # first bin is "extra" for "reasons" + rmag_centers = np.array(hf['rmag_centers']) + p_mr_sfr = np.array(hf['p_mr_sfr']) + p_mr_mass = np.array(hf['p_mr_mass']) + + # normalise these probabilities such that the bins sum to unity + p_mr_sfr = (p_mr_sfr.T / np.sum(p_mr_sfr,axis=1)).T + p_mr_mass = (p_mr_mass.T / np.sum(p_mr_mass,axis=1)).T + + print(f"p(mr|z) distributions loaded successfully from 'p_mr_dists/{fname}'") + n_redshift_bins = len(zbins) - 1 + return zbins, rmag_centers, p_mr_sfr, p_mr_mass + + def give_p_mr_mass(self,z: float): + """ + Function to return p(mr|z) for mass-weighted population. + Args: + z (float): Redshift value. + Returns: + np.array: p(mr|z) values. + Note: + This function assumes that the redshift bins are defined in the `massweighted_population` data. + Given the fine discretization of redshift bins, it uses the nearest bin for the provided redshift value. + rmag_centers and p_mr_mass are defined in the outer scope of this function. + """ + # Find the appropriate redshift bin index + idx = np.clip(np.searchsorted(self.zbins, z) - 1, 0, n_redshift_bins - 1) + return self.p_mr_mass[idx] + + def give_p_mr_sfr(self,z: float): + """ + Function to return p(mr|z) for SFR-weighted population. + Args: + z (float): Redshift value. + Returns: + np.array: p(mr|z) values. + Note: + This function assumes that the redshift bins are defined in the `sfrweighted_population` data. + Given the fine discretization of redshift bins, it uses the nearest bin for the provided redshift value. + rmag_centers and p_mr_sfr are defined in the outer scope of this function. + """ + # Find the appropriate redshift bin index + idx = np.clip(np.searchsorted(self.zbins, z) - 1, 0, n_redshift_bins - 1) + return self.p_mr_sfr[idx] + + def init_args(self,OpticalState): + """ + Initialises prior based on sfr fraction + + Args: + opstate: optical model state. Grabs the Loudas parameters from there. + + """ + self.OpticalState = OpticalState + self.opstate = OpticalState.loudas + self.fsfr = self.opstate.fSFR + + def init_priors(self,zlist): + """ + Generates magniude prior distributions for a list of redshifts + This allows faster interpolation later. + + Currently, this is not used! + """ + print("WARNING: redundant init priors!!!!!!") + exit() + mass_priors = np.zeros([zlist.size,self.nmr]) + sfr_priors = np.zeros([zlist.size,self.nmr]) + for i,z in enumerate(zlist): + mass_priors[i,:] = self.get_p_mr(z,0.) + sfr_priors[i,:] = self.get_p_mr(z,1.) + self.mass_priors = mass_priors + self.sfr_priors = sfr_priors + +class simple_host_model: """ A class to hold information about the intrinsic properties of FRB - host galaxies. Eventually, this should be expanded to be a - meta-class with different internal models. But for now, it's - just a simple one + host galaxies. This is a simple but generic model. Ingredients are: A model for describing the intrinsic distribution of host galaxies. This model must be described by some set of parameters, and be able to return a prior as a function of intrinsic host galaxy magnitude. - This model is initialised via opstate.AbsModelID + This model is initialised via opstate.AbsModelID. + Here, it is just 10 parameters at different absolute + magnitudes, with linear/spline interpolation A model for converting absolute to apparent host magnitudes. This is by defult an apparent r-band magnitude, though @@ -40,18 +352,9 @@ class host_model: Internally, this class initialises: An array of absolute magnitudes, which get weighted according - to the host model. - Internal variables associated with this are prefaced "Model" - - An array of apparent magnitudes, which is used to compare with - host galaxy candidates - Internal variables associated with this are prefaced "App" + to the host model. Internal variables associated with this + are prefaced "Model" - Arrays mapping intrinsic to absolute magnitude as a function - of redshift, to allow quick estimation of p(apparent_mag | DM) - for a given FRB survey with many FRBs - Internal variables associated with this are prefaced "Abs" - Note that while this class describes the intrinsic "magnitudes", really magnitude here is a proxy for whatever parameter is used to intrinsically describe FRBs. However, only 1D descriptions are @@ -59,9 +362,9 @@ class host_model: evolution, and 2D descriptions (e.g. mass, SFR) at any given redshift. """ - def __init__(self,opstate=None,verbose=False): + def __init__(self,OpticalState=None,verbose=False): """ - Class constructor + Class constructor. Args: opstate (class: Hosts, optional): class defining parameters @@ -69,49 +372,42 @@ def __init__(self,opstate=None,verbose=False): verbose (bool, optional): to be verbose y/n """ - if opstate is None: - opstate = op.Hosts() + # uses the "simple hosts" descriptor + if OpticalState is None: + self.OpticalState = op.OpticalState() + else: + self.OpticalState = OpticalState + self.opstate = self.OpticalState.simple + # checks that cosmology is initialised + if not cos.INIT: + cos.init_dist_measures() - if opstate.AppModelID == 0: + if self.opstate.AppModelID == 0: if verbose: print("Initialising simple luminosity function") # must take arguments of (absoluteMag,z) self.CalcApparentMags = SimpleApparentMags else: - raise ValueError("Model ",opstate.AppModelID," not implemented") + raise ValueError("Model ",self.opstate.AppModelID," not implemented") - if opstate.AbsModelID == 0: + if self.opstate.AbsModelID == 0: if verbose: print("Describing absolute mags with N independent bins") - elif opstate.AbsModelID == 1: + elif self.opstate.AbsModelID == 1: if verbose: print("Describing absolute mags with spline interpoilation of N points") else: - raise ValueError("Model ",opstate.AbsModelID," not implemented") + raise ValueError("Model ",self.opstate.AbsModelID," not implemented") + + self.AppModelID = self.opstate.AppModelID + self.AbsModelID = self.opstate.AbsModelID - self.AppModelID = opstate.AppModelID - self.AbsModelID = opstate.AbsModelID - self.opstate = opstate self.init_abs_bins() self.init_model_bins() - self.init_app_bins() - self.init_abs_prior() - - self.ZMAP = False # records that we need to initialise this - - ############################################################# - ################## Initialisation Functions ################# - ############################################################# - - def init_abs_prior(self): - """ - Initialises prior on absolute magnitude of galaxies according to the method. - - """ if self.opstate.AbsPriorMeth==0: # uniform prior in log space of absolute magnitude @@ -120,43 +416,17 @@ def init_abs_prior(self): # other methods to be added as required raise ValueError("Luminosity prior method ",self.opstate.AbsPriorMeth," not implemented") - # enforces normalisation of the prior to unity - Absprior /= np.sum(Absprior) - self.AbsPrior = Absprior - - - # this maps the weights from the parameter file to the absoluate magnitudes use - # internally within the program. We now initialise this during an "init" - self.AbsMagWeights = self.init_abs_mag_weights() - - # renormalises the weights, so all internal apparent mags sum to unit - # include this step in the init routine perhaps? - self.AbsMagWeights /= np.sum(self.AbsMagWeights) - - def init_app_bins(self): - """ - Initialises bins in apparent magnitude - It uses these to calculate priors for any given host galaxy magnitude. - This is a very simple set of uniformly log-spaced bins in magnitude space, - and linear interpolation is used between them. - """ - - self.Appmin = self.opstate.Appmin - self.Appmax = self.opstate.Appmax - self.NAppBins = self.opstate.NAppBins - - # this creates the bin edges - self.AppBins = np.linspace(self.Appmin,self.Appmax,self.NAppBins+1) - dAppBin = self.AppBins[1] - self.AppBins[0] - self.AppMags = self.AppBins[:-1] + dAppBin/2. - self.dAppmag = dAppBin + self.init_args(Absprior) + # the below is done for the wrapper function + #self.ZMAP = False # records that we need to initialise this + def init_abs_bins(self): """ Initialises internal array of absolute magnitudes This is a simple set of uniformly log-spaced bins in terms of absolute magnitude, which the absolute magnitude model gets - projected onto + projected onto. """ # shortcuts Absmin = self.opstate.Absmin @@ -177,6 +447,25 @@ def init_abs_bins(self): self.dMag = dMag self.AbsMags = AbsMags + def init_args(self,AbsPrior): + """ + Initialises prior on absolute magnitude of galaxies according to the method. + + Args: + - AbsPrior (list of floats): The prior on absolute magnitudes + to set for this model + + """ + # Eventually, incorporate the AbsPrior vector into SimpleParams + #self.opstate = OpticalState.SimpleParams + + # enforces normalisation of the prior to unity + self.AbsPrior = AbsPrior/np.sum(AbsPrior) + + # this maps the weights from the parameter file to the absoluate magnitudes use + # internally within the program. We now initialise this during an "init" + self.init_abs_mag_weights() + def init_model_bins(self): """ Initialises bins for the simple model of an absolute @@ -203,7 +492,7 @@ def init_model_bins(self): self.ModelBins = ModelBins self.dModel = ModelBins[1]-ModelBins[0] - def init_zmapping(self,zvals): + def get_pmr_gz(self,mrbins,z): """ For a set of redshifts, initialise mapping between intrinsic magnitudes and apparent magnitudes @@ -215,63 +504,40 @@ def init_zmapping(self,zvals): with a set of z values. This is all for speedup purposes. Args: + mrbins (np.array, float): array of apparent magnitudes (mr) + over which to calculate p(mr). These act as bins + in apparent magnitude mr for histogram purposes, + i.e. they are not probabilities *at* mr zvals (np.ndarray, float): array of redshifts over which to map absolute to apparent magnitudes. """ - # records that this has been initialised - self.ZMAP = True - # mapping of apparent to absolute magnitude - self.zmap = self.CalcApparentMags(self.AbsMags,zvals) - self.zvals = zvals - self.NZ = self.zvals.size + mrvals = self.CalcApparentMags(self.AbsMags,z) # works with scalar z - self.init_maghist() - - def init_maghist(self): - """ - Initialises the array mapping redshifts and absolute magnitudes - to redshift and apparent magnitude - Calculates the internal maghist array, of size self.NAppBins X self.NZ + # creates weighted histogram of apparent magnitudes, + # using model weights from wmap (which are fixed for all z) + hist,bins = np.histogram(mrvals,weights=self.AbsMagWeights,bins=mrbins) - No return value. - - """ + #smoothing function - just to flatten the params + NS=10 + smoothf = self.gauss(mrvals[0:NS] - np.average(mrvals[0:NS])) + smoothf /= np.sum(smoothf) + smoothed = np.convolve(hist,smoothf,mode="same") - # for current model, calculate weighted histogram of apparent magnitude - # for each redshift. Done by converting intrinsic to apparent for each z, - # then suming up the associated weights - maghist = np.zeros([self.NAppBins,self.NZ]) - for i in np.arange(self.NZ): - # creates weighted histogram of apparent magnitudes, - # using model weights from wmap (which are fixed for all z) - hist,bins = np.histogram(self.zmap[:,i],weights=self.AbsMagWeights,bins=self.AppBins) - - # # NOTE: these should NOT be re-normalised, since the normalisation reflects - # true magnitudes which fall off the apparent magnitude histogram. - maghist[:,i] = hist - - self.maghist = maghist + smoothed=hist - def reinit_model(self): + # # NOTE: these should NOT be re-normalised, since the normalisation reflects + # true magnitudes which fall off the apparent magnitude histogram. + return smoothed + + def gauss(self,x,mu=0,sigma=0.1): """ - Re-initialises all internal info which depends on the optical - param model. It assumes that the changes have been implemented in - self.AbsPrior + simple Gaussian smoothing function """ + return np.exp(-0.5*(x-mu)**2/sigma**2) - # this maps the weights from the parameter file to the absoluate magnitudes use - # internally within the program. We now initialise this during an "init" - self.AbsMagWeights = self.init_abs_mag_weights() - - # renormalises the weights, so all internal apparent mags sum to unity - # include this step in the init routine perhaps? - self.AbsMagWeights /= np.sum(self.AbsMagWeights) - - self.init_maghist() - def init_abs_mag_weights(self): """ Assigns a weight to each of the absolute magnitudes @@ -298,17 +564,216 @@ def init_abs_mag_weights(self): # coefficients span full range cs = CubicSpline(self.ModelBins,self.AbsPrior) weights = cs(self.AbsMags) + # ensures no negatives toolow = np.where(weights < 0.) weights[toolow] = 0. + # ensures that if everything is zero above/below a point, so is the interpolation + iFirstNonzero = np.where(self.AbsPrior > 0.)[0][0] + if iFirstNonzero > 0: + toolow = np.where(self.AbsMags < self.ModelBins[iFirstNonzero -1]) + weights[toolow] = 0. + iLastNonzero = np.where(self.AbsPrior > 0.)[0][-1] + if iLastNonzero < self.AbsPrior.size - 1: + toohigh = np.where(self.AbsMags > self.ModelBins[iLastNonzero+1]) + weights[toohigh] = 0. + else: raise ValueError("This weighting scheme not yet implemented") - return weights + + + + # renormalises the weights, so all internal apparent mags sum to unit + self.AbsMagWeights = weights / np.sum(weights) + + return + + +class model_wrapper: + """ + Generic functions applicable to all models. + + The program flow is to initialise with a host model ("model"), + then given arrays of Mr and zvalues, pre-calculate an array + of p(Mr|z), and then for individual host galaxies with a + p(z|DM) distribution, be able to return priors for PATH. + + Internally, the code uses an array of apparent magnitudes, + which is used to compare with host galaxy candidates. + Internal variables associated with this are prefaced "App" + + The Arrays mapping intrinsic to absolute magnitude as a function + of redshift, to allow quick estimation of p(apparent_mag | DM) + for a given FRB survey with many FRBs + Internal variables associated with this are prefaced "Abs" + The workflow is: + -init with a model class and array of z values. This sets + absolute magnitude bins. The z values should correspond + to those from a grid object. + Initialisation primarily calls p(Mr|z) repeatedly for all internal + Mr and z values, to allow fast evaluation in the future + - set up PATH functions to point to this array: + pathpriors.USR_raw_prior_Oi = wrapper.path_raw_prior_Oi + - initialise this class for a given init_path_raw_prior_Oi(DM,grid). + This calculates magnitude priors given p(z|DM) (grid) + and p(mr|z) (host model). + + """ + def __init__(self,model,zvals): + """ + Initialises model wrapper. + + + Args: + model (class object): Model is one of the host model class objects + that can calculate p(Mr|z) + zvals (np.array): redshift values corresponding to grid object + opstate (class optical): state containing optical info + + """ + + # higher level state defining optical parameters + self.OpticalState = model.OpticalState + + # specific substate of the model + self.opstate = model.opstate + + self.model = model # checks the model has required attributes + + + # initialise bins in apparent magnitude + self.init_app_bins() + + self.init_zmapping(zvals) + + + def init_app_bins(self): + """ + Initialises bins in apparent magnitude + It uses these to calculate priors for any given host galaxy magnitude. + This is a very simple set of uniformly log-spaced bins in magnitude space, + and linear interpolation is used between them. + """ + + + self.Appmin = self.OpticalState.app.Appmin + self.Appmax = self.OpticalState.app.Appmax + self.NAppBins = self.OpticalState.app.NAppBins + + # this creates the bin edges + self.AppBins = np.linspace(self.Appmin,self.Appmax,self.NAppBins+1) + dAppBin = self.AppBins[1] - self.AppBins[0] + self.AppMags = self.AppBins[:-1] + dAppBin/2. + self.dAppmag = dAppBin + + def init_zmapping(self,zvals): + """ + For a set of redshifts, initialise mapping + between intrinsic magnitudes and apparent magnitudes + + This routine only needs to be called once, since the model + to convert absolute to apparent magnitudes is fixed + + It is not set automatically however, and needs to be called + with a set of z values. This is all for speedup purposes. + + Args: + zvals (np.ndarray, float): array of redshifts over which + to map absolute to apparent magnitudes. + """ + + self.zvals=zvals + + # we aim to produce a grid of p(z,m_r) for rapid convolution + # with a p(z) array + self.nz = zvals.size + + p_mr_z = np.zeros([self.NAppBins,self.nz]) + + for i,z in enumerate(zvals): + # use the model to calculate p(mr|z) for range of z-values + # this is then stored in an array + p_mr_z[:,i] = self.model.get_pmr_gz(self.AppBins,z) + + self.p_mr_z = p_mr_z + + + + # records that this has been initialised + self.ZMAP = True + ############################################################# ################## Path Calculations ################# ############################################################# + + def init_path_raw_prior_Oi(self,DM,grid): + """ + Initialises the priors for a particlar DM. + This performs a function very similar to + "get_posterior" except that it expicitly + only operates on a single DM, and saves the + information internally so that + path_raw_prior_Oi can be called for numerous + host galaxy candidates. + + It returns the priors distribution. + + Args: + DM [float]: dispersion measure of an FRB (pc cm-3) + grid (class grid): initialised grid object from which + to calculate priors + + Returns: + priors (float): vector of priors on host galaxy apparent magnitude + """ + + # we start by getting the posterior distribution p(z) + # for an FRB with DM DM seen by the 'grid' + pz = get_pz_prior(grid,DM) + + # checks that pz is normalised + pz /= np.sum(pz) + + priors = np.sum(self.p_mr_z * pz,axis=1) # sums over z + + # stores knowledge of the DM used to calculate the priors + self.prior_DM = DM + self.priors = priors + + #return priors + + def get_posterior(self, grid, DM): + """ + Similar functionality to init_path_raw_prior_Oi. May be legacy code. + + Returns posterior redshift distributiuon for a given grid, and DM + magnitude distribution, for FRBs of DM given a grid object. + Note: this calculates a prior for PATH, but is a posterior + from zDM's point of view. + + Args: + grid (class grid object): grid object defining p(z,DM) + DM (float, np.ndarray OR scalar): FRB DM(s) + + Returns: + papps (np.ndarray, floats): probability distribution of apparent magnitudes given DM + pz (np.ndarray, floats): probability distribution of redshift given DM + """ + # Step 1: get prior on z + pz = get_pz_prior(grid,DM) + + ### STEP 2: get apparent magnitude distribution ### + if hasattr(DM,"__len__"): + papps = np.dot(self.maghist,pz) + else: + papps = self.maghist*pz + + + return papps,pz + + def estimate_unseen_prior(self,mag_limit): """ Calculates PU, the prior that an FRB host galaxy of a @@ -391,98 +856,10 @@ def path_raw_prior_Oi(self,mags,ang_sizes,Sigma_ms): Ois = np.array(Ois) return Ois - def init_path_raw_prior_Oi(self,DM,grid): - """ - Initialises the priors for a particlar DM. - This performs a function very similar to - "get_posterior" except that it expicitly - only operates on a single DM, and saves the - information internally so that - path_raw_prior_Oi can be called for numerous - host galaxy candidates. - - It returns the priors distribution. - - Args: - DM [float]: dispersion measure of an FRB (pc cm-3) - grid (class grid): initialised grid object from which - to calculate priors - - Returns: - priors (float): vector of priors on host galaxy apparent magnitude - """ - - # we start by getting the posterior distribution p(z) - # for an FRB with DM DM seen by the 'grid' - pz = get_pz_prior(grid,DM) - - # we now calculate the list of priors - for the array - # defined by self.AppBins with bin centres at self.AppMags - priors = self.calc_magnitude_priors(grid.zvals,pz) - - # stores knowledge of the DM used to calculate the priors - self.prior_DM = DM - self.priors = priors - - return priors - - - def calc_magnitude_priors(self,zlist:np.ndarray,pzlist:np.ndarray): - """ - Calculates priors as a function of magnitude for - a given redshift distribution. - - Args: - zlist (np.ndarray, float): array of redshifts - pz (np.ndarray, float): array of probabilities of the FRB - occurring at each of those redshifts - - # returns probability-weighted magnitude distribution, as a function of - # self.AppBins - - """ - # we integrate over the host absolute magnitude distribution - - # checks that pz is normalised - pzlist /= np.sum(pzlist) - - for i,absmag in enumerate(self.AbsMags): - plum = self.AbsMagWeights[i] - mags = self.CalcApparentMags(absmag,zlist) - temp,bins = np.histogram(mags,weights=pzlist*plum,bins=self.AppBins) - if i==0: - pmags = temp - else: - pmags += temp - - return pmags - - def get_posterior(self, grid, DM): - """ - Returns posterior redshift distributiuon for a given grid, and DM - magnitude distribution, for FRBs of DM given a grid object. - Note: this calculates a prior for PATH, but is a posterior - from zDM's point of view. - - Args: - grid (class grid object): grid object defining p(z,DM) - DM (float, np.ndarray OR scalar): FRB DM(s) - - Returns: - papps (np.ndarray, floats): probability distribution of apparent magnitudes given DM - pz (np.ndarray, floats): probability distribution of redshift given DM - """ - # Step 1: get prior on z - pz = get_pz_prior(grid,DM) - - ### STEP 2: get apparent magnitude distribution ### - if hasattr(DM,"__len__"): - papps = np.dot(self.maghist,pz) - else: - papps = self.maghist*pz - - - return papps,pz + + + +################# Useful functions not associated with a class ######### def get_pz_prior(grid, DM): """ @@ -664,7 +1041,7 @@ def matchFRB(TNSname,survey): -def run_path(name,model,PU=0.1,usemodel = False, sort = False): +def run_path(name,PU=0.1,usemodel = False, sort = False): """ evaluates PATH on an FRB @@ -824,18 +1201,3 @@ def load_marnoch_data(): return table -############ Routines associated with Nick's model ################ - -class host_model: - """ - This class initiates a model based on ... - """ - - - - def - -def gen_mag_dist(z,f): - """ - generates a magnitude distribution as a function of z and f - """ diff --git a/zdm/optical_params.py b/zdm/optical_params.py index 86b9da89..de98e454 100644 --- a/zdm/optical_params.py +++ b/zdm/optical_params.py @@ -1,12 +1,22 @@ -""" Classes for optical properties """ +""" +Classes for optical properties + +This philosophy here is to have a class of key parameters that relates to +a single class object contained within optical.py. The dataclasses are +used to set parameters that initialise their parent classes, which is +where all the complicated calculations are performed. +""" from dataclasses import dataclass, field from zdm import data_class import numpy as np + + +# Simple SFR model @dataclass -class Hosts(data_class.myDataClass): +class SimpleParams(data_class.myDataClass): """ Data class to hold the generic host galaxy class with no pre-specified model @@ -37,24 +47,6 @@ class Hosts(data_class.myDataClass): 'unit': '', 'Notation': '', }) - Appmin: float = field( - default=10, - metadata={'help': "Minimum host apparent magnitude", - 'unit': 'm_r^{min}', - 'Notation': '', - }) - Appmax: float = field( - default=35, - metadata={'help': "Maximum host apparent magnitude", - 'unit': 'm_r^{max}', - 'Notation': '', - }) - NAppBins: int = field( - default=250, - metadata={'help': "Number of apparent magnitude bins", - 'unit': '', - 'Notation': '', - }) AbsPriorMeth: int = field( default=0, metadata={'help': "Model for abs mag prior and function description. 0: uniform distribution. Others to be implemented.", @@ -68,16 +60,19 @@ class Hosts(data_class.myDataClass): 'Notation': '', }) AbsModelID: int = field( - default=0, + default=1, metadata={'help': "Model for describing absolute magnitudes. 0: Simple histogram of absolute magnitudes. 1: spline interpolation of histogram.", 'unit': '', 'Notation': '', }) -class SFRmodel(data_class.myDataClass): + +# Nick Loudas's SFR model +@dataclass +class LoudasParams(data_class.myDataClass): """ Data class to hold the SFR model from Nick, which models - FRBs as some fraction of the star-formation rate + FRBs as some fraction of the star-formation rate. """ fSFR: float = field( default=0.5, @@ -121,3 +116,50 @@ class SFRmodel(data_class.myDataClass): 'unit': '', 'Notation': '', }) + + +@dataclass +class Apparent(data_class.myDataClass): + """ + # parameters for apparent mags - used by wrapper + """ + Appmin: float = field( + default=10, + metadata={'help': "Minimum host apparent magnitude", + 'unit': 'm_r^{min}', + 'Notation': '', + }) + Appmax: float = field( + default=35, + metadata={'help': "Maximum host apparent magnitude", + 'unit': 'm_r^{max}', + 'Notation': '', + }) + NAppBins: int = field( + default=250, + metadata={'help': "Number of apparent magnitude bins", + 'unit': '', + 'Notation': '', + }) + +class OpticalState(data_class.myData): + """Initialize the full optical state dataset + with the default parameters + + """ + + def __init__(self): + self.set_dataclasses() + self.set_params() + + def set_dataclasses(self): + self.simple = SimpleParams() + self.loudas = LoudasParams() + self.app = Apparent() + + + def update_param(self, param:str, value): + # print(self.params) + DC = self.params[param] + setattr(self[DC], param, value) + diff --git a/zdm/parameters.py b/zdm/parameters.py index 73fcfdae..9fcf5877 100644 --- a/zdm/parameters.py +++ b/zdm/parameters.py @@ -116,7 +116,7 @@ class FRBDemoParams(data_class.myDataClass): ) lC: float = field( default=3.3249, - metadata={"help": "log10 constant in number per Gpc^-3 day^-1 at z=0"}, + metadata={"help": "log10 constant in number per Mpc^-3 day^-1 at z=0"}, ) @@ -144,8 +144,8 @@ class RepeatParams(data_class.myDataClass): }) RC: float = field( default = 1e-2, - metadata={'help': 'Constant repeater density', - 'unit': 'Repeaters day / Gpc^-3', + metadata={'help': 'Constant repeater density. Gets calculated by code.', + 'unit': 'Repeaters / Mpc^-3', 'Notation': '$C_R$', }) RE0: float = field( diff --git a/zdm/scripts/Path/plot_host_models.py b/zdm/scripts/Path/plot_host_models.py new file mode 100644 index 00000000..8c7256e3 --- /dev/null +++ b/zdm/scripts/Path/plot_host_models.py @@ -0,0 +1,297 @@ +""" +Script showing how to load different FRB host models. + +It does this for both my simple naive model, and Nick's +model based on mass- or sfr-weightings. + +We then evaluate P(O|x) for CRAFT FRBs in the CRAFT 1300 MHz survey + +""" + +#standard Python imports +import numpy as np +from matplotlib import pyplot as plt + +# imports from the "FRB" series +from zdm import optical as opt +from zdm import optical_params as op +from zdm import loading +from zdm import cosmology as cos +from zdm import parameters +from zdm import loading + +import astropath.priors as pathpriors + + +def calc_path_priors(): + """ + Loops over all ICS FRBs + """ + + opdir = "Plots/" + + ##### performs the following calculations for the below combinations ###### + + # loads a default optical state. + opstate1 = op.OpticalState() + #opstate1.SimpleParams.AbsPrior = [0,0,0.1,1,0.4,0,0,0,0,0] # this is from an initial estimate + + opstate2 = op.OpticalState() + opstate2.loudas.fSFR=0. + + + ######## initialises optical-independent info ######## + #frblist is a hard-coded list of FRBs for which we have optical PATH data + frblist = opt.frblist + NFRB = len(frblist) + + + state = parameters.State() + cos.set_cosmology(state) + cos.init_dist_measures() + + + plt.figure() + + opstate = op.SimpleParams() + + ##### makes a plot of host priors for the simple model ###### + + # simple host model + model1 = opt.simple_host_model(opstate1) + # this is from an initial estimate. Currently, no way to enter this into the opstate. To do. + absprior = [0,0,0.1,1,0.4,0,0,0,0,0] + model1.init_args(absprior) + + plt.figure() + plt.plot(model1.AbsMags,model1.AbsMagWeights/np.max(model1.AbsMagWeights),label="Histogram interpolation") + plt.scatter(model1.ModelBins,model1.AbsPrior/np.max(model1.AbsPrior),label="Simple model points") + plt.xlabel("Absolute magnitude $M_r$") + plt.ylabel("$p(M_r)$") + plt.legend() + plt.tight_layout() + plt.savefig(opdir+"simple_model_mags.png") + plt.close() + + ####### Makes plots for Nick Loudas' model ##### + + model2=opt.loudas_model(opstate2) + for i in np.arange(1,20,4): + plt.plot(model2.rmags,model2.p_mr_mass[i],label="$M_\\odot$, z="+str(model2.zbins[i]),linestyle="-") + plt.plot(model2.rmags,model2.p_mr_sfr[i],label="SFR, z="+str(model2.zbins[i]),linestyle="--",color=plt.gca().lines[-1].get_color()) + plt.xlabel("apparent magnitude $m_r$") + plt.ylabel("$p(m_r)$") + plt.legend() + plt.tight_layout() + plt.savefig(opdir+"loudas_model_mags.png") + plt.close() + + + + # set up basic histogram of p(mr) distribution + mrbins = np.linspace(0,30,301) + mrvals=(mrbins[:-1]+mrbins[1:])/2. + + + + ######### Plots apparent mag distribution for all models as function of z ####### + styles=["-","--",":","-."] + + plt.figure() + flist=[0,0.5,1.] + + for z in [0.1,0.5,2]: + + # simple model + pmr = model1.get_pmr_gz(mrbins,z) + pmr /= np.sum(pmr) + plt.plot(mrvals,pmr,label="Simple: z = "+str(z),linestyle=styles[0]) + + # Loudas model dependencies + for i,fsfr in enumerate(flist): + opstate2.loudas.fSFR = fsfr # mass-dependent + model2.init_args(opstate2) + pmr = model2.get_pmr_gz(mrbins,z) + pmr /= np.sum(pmr) + plt.plot(mrvals,pmr,label="z = "+str(z)+", $f_{\\rm sfr}$ = "+str(fsfr),linestyle=styles[i+1],color=plt.gca().lines[-1].get_color()) + + plt.xlabel("Optical magnitude $m_r$") + plt.ylabel("p(m_r|z)") + plt.tight_layout() + plt.legend() + plt.savefig(opdir+"all_model_apparent_mags.png") + plt.close() + + + + ############################################################################ + #Load a grid. We'll only load data from the ICS 1300 survey. Just to use it + # as an example calculation for particular DMs + ############################################################################ + name='CRAFT_ICS_1300' + ss,gs = loading.surveys_and_grids(survey_names=[name]) + g = gs[0] + s = ss[0] + + + # wrapper around the optical model. For returning p(m_r|DM) + wrapper1 = opt.model_wrapper(model1,g.zvals) # simple + wrapper2 = opt.model_wrapper(model2,g.zvals) # loudas with fsfr=0 + + # do this once per "model" objects + #pathpriors.USR_raw_prior_Oi = wrapper1.path_raw_prior_Oi + + + # how do we change a parameter? We need to pass on the low-level model to the wrapper + plt.figure() + + for i,DM in enumerate([200,600,1000]): + + wrapper1.init_path_raw_prior_Oi(DM,g) + plt.plot(wrapper1.AppMags,wrapper1.priors,label="DM = "+str(DM)+", Simple",linestyle=styles[0]) + + # this is how we change the parameters of a state + # we first change the underlying state + # then we initialise the model + # then we re-init the wrapper. + opstate2.loudas.fSFR=0. + model2.init_args(opstate2) + wrapper2.init_zmapping(g.zvals) + wrapper2.init_path_raw_prior_Oi(DM,g) + plt.plot(wrapper2.AppMags,wrapper2.priors,label="DM = "+str(DM)+", $f_{\\rm sfr}$ = 0.0", + linestyle=styles[1],color=plt.gca().lines[-1].get_color()) + + opstate2.loudas.fSFR=1.0 + model2.init_args(opstate2) + wrapper2.init_zmapping(g.zvals) + wrapper2.init_path_raw_prior_Oi(DM,g) + plt.plot(wrapper2.AppMags,wrapper2.priors,label="DM = "+str(DM)+", $f_{\\rm sfr}$ = 1.0", + linestyle=styles[2],color=plt.gca().lines[-1].get_color()) + + plt.xlabel("Absolute magnitude $M_r$") + plt.ylabel("$p(M_r)$") + plt.legend() + plt.tight_layout() + plt.savefig(opdir+"all_models_mag_priors_dm.png") + plt.close() + + # do this only for a particular FRB + # it gives a prior on apparent magnitude and pz + #AppMagPriors,pz = model.get_posterior(g,DMlist) + + + maglist = [None,None,None,None] + allPOx = [None,None,None,None] + + labels=["Orig","Simple","Mass-weighted","SFR weighted"] + markers=["x","+","s","o"] + + for i,frb in enumerate(frblist): + # interates over the FRBs. "Do FRB" + # P_O is the prior for each galaxy + # P_Ox is the posterior + # P_Ux is the posterior for it being unobserved + # mags is the list of galaxy magnitudes + + # determines if this FRB was seen by the survey, and + # if so, what its DMEG is + imatch = opt.matchFRB(frb,s) + if imatch is None: + print("Could not find ",frb," in survey") + continue + else: + print("Found FRB ",frb) + + DMEG = s.DMEGs[imatch] + + # original calculation + P_O1,P_Ox1,P_Ux1,mags1 = opt.run_path(frb,usemodel=False,PU=0.1) + + # record this info + if maglist[0] is None: + maglist[0] = mags1 + allPOx[0] = P_Ox1 + else: + maglist[0] = np.append(maglist[0],mags1) + allPOx[0] = np.append(allPOx[0],P_Ox1) + + + # simple model + wrapper1.init_path_raw_prior_Oi(DMEG,g) + PU2 = wrapper1.estimate_unseen_prior(mag_limit=26) # might not be correct + pathpriors.USR_raw_prior_Oi = wrapper1.path_raw_prior_Oi + P_O2,P_Ox2,P_Ux2,mags2 = opt.run_path(frb,usemodel=True,PU = PU2) + + + for imag,mag in enumerate(mags2): + if P_Ox2[imag] > 0.5 and P_Ox1[imag] < 0.5: + #print(i,frb,mag,P_Ox1[imag],P_Ox2[imag]) + print(frb,P_Ux1,PU2,DMEG) + for k,x in enumerate(P_Ox1): + print(mags1[k],x,P_Ox2[k]) + + # record this info + if maglist[1] is None: + maglist[1] = mags2 + allPOx[1] = P_Ox2 + else: + maglist[1] = np.append(maglist[1],mags2) + allPOx[1] = np.append(allPOx[1],P_Ox2) + + + # loudas fsfr = 0.0 (i.e., mass weighted) + opstate2.loudas.fSFR=0.0 + model2.init_args(opstate2) + wrapper2.init_zmapping(g.zvals) + wrapper2.init_path_raw_prior_Oi(DMEG,g) + PU3 = wrapper2.estimate_unseen_prior(mag_limit=26) # might not be correct + pathpriors.USR_raw_prior_Oi = wrapper2.path_raw_prior_Oi + P_O3,P_Ox3,P_Ux3,mags3 = opt.run_path(frb,usemodel=True,PU = PU3) + + # record this info + if maglist[2] is None: + maglist[2] = mags3 + allPOx[2] = P_Ox3 + else: + maglist[2] = np.append(maglist[2],mags3) + allPOx[2] = np.append(allPOx[2],P_Ox3) + + + # loudas fsfr = 1.0 + opstate2.loudas.fSFR=1.0 + model2.init_args(opstate2) + wrapper2.init_zmapping(g.zvals) + wrapper2.init_path_raw_prior_Oi(DMEG,g) + PU4 = wrapper2.estimate_unseen_prior(mag_limit=26) # might not be correct limit + pathpriors.USR_raw_prior_Oi = wrapper2.path_raw_prior_Oi + P_O4,P_Ox4,P_Ux4,mags4 = opt.run_path(frb,usemodel=True,PU = PU4) + + # record this info + if maglist[3] is None: + maglist[3] = mags4 + allPOx[3] = P_Ox4 + else: + maglist[3] = np.append(maglist[3],mags4) + allPOx[3] = np.append(allPOx[3],P_Ox4) + + + # scatter plot of old vs new priors + plt.figure() + plt.xlabel("$P(O|x)$ (original)") + plt.ylabel("$P(O|x)$ (new)") + for j in np.arange(1,4,1): + plt.scatter(allPOx[0],allPOx[j],label=labels[j],marker=markers[j]) + plt.legend() + plt.tight_layout() + plt.savefig(opdir+"posterior_comparison.png") + plt.close() + + + + +if __name__ == "__main__": + + calc_path_priors() + + + diff --git a/zdm/scripts/PlotIndividualExperiments/plot_Meertrap.py b/zdm/scripts/PlotIndividualExperiments/plot_Meertrap.py index 89d68449..79cb0cac 100644 --- a/zdm/scripts/PlotIndividualExperiments/plot_Meertrap.py +++ b/zdm/scripts/PlotIndividualExperiments/plot_Meertrap.py @@ -36,7 +36,7 @@ def main(): # approximate best-fit values from recent analysis # load states from Hoffman et al 2025 state = states.load_state("HoffmannEmin25",scat="updated",rep=None) - opdir="MeerTRAP" + opdir="MeerTRAP/" if not os.path.exists(opdir): os.mkdir(opdir) @@ -63,7 +63,7 @@ def main(): name = names[0] figures.plot_grid(g.rates,g.zvals,g.dmvals, name=opdir+name+"_zDM.pdf",norm=3,log=True, - label='$\\log_{10} p({\\rm DM}_{\\rm IGM} + {\\rm DM}_{\\rm host},z)$ [a.u.]', + label='$\\log_{10} p({\\rm DM}_{\\rm cosmic} + {\\rm DM}_{\\rm host},z)$ [a.u.]', project=False,ylabel='${\\rm DM}_{\\rm IGM} + {\\rm DM}_{\\rm host}$', zmax=zmax,DMmax=DMmax,Aconts=[0.01,0.1,0.5]) From 828b7554fdf0c598f30371e365e997fb34742ec5 Mon Sep 17 00:00:00 2001 From: Clancy James Date: Mon, 5 Jan 2026 08:55:35 +0800 Subject: [PATCH 08/35] Successful implementation of loudas model optimisation --- zdm/optical.py | 149 ++++++++++-- zdm/optical_numerics.py | 234 +++++++++++++++++++ zdm/scripts/Path/optimise_host_priors.py | 286 ++++++----------------- zdm/scripts/Path/plot_host_models.py | 84 ++++--- 4 files changed, 494 insertions(+), 259 deletions(-) create mode 100644 zdm/optical_numerics.py diff --git a/zdm/optical.py b/zdm/optical.py index e9a2acfa..7250b3fb 100644 --- a/zdm/optical.py +++ b/zdm/optical.py @@ -51,16 +51,114 @@ from zdm import optical_params as op from scipy.interpolate import CubicSpline from scipy.interpolate import make_interp_spline +from scipy.stats import norm import os from importlib import resources import pandas import h5py +import astropath.priors as pathpriors + ################################################################### ############ Routines associated with Nick's model ################ ################################################################### + +class marnoch_model: + """ + Class initiates a model based on Lachlan Marnoch's predictions + for FRB host galaxy visibility in + https://ui.adsabs.harvard.edu/abs/2023MNRAS.525..994M/abstract + Here, we assume that host galaxy magnitudes have a normal + distribution, with mean and standard deviation given by + L. Marnoch's data. + """ + + def __init__(self,OpticalState=None): + """ + Initialises the model. There are no variables here. + + Args: + OpticalState: allows the model to refer to an optical state. + However, the model is independent of that state. + """ + + # uses the "simple hosts" descriptor + if OpticalState is None: + OpticalState = op.OpticalState() + self.OpticalState = OpticalState + self.opstate = None + + # loads the dataset + self.load_data() + + # extracts subic splines for mean and std dev + self.process_rbands() + + + def load_data(self): + """ + Loads the Marnoch et al data on r-band magnitudes from FRB hosts + """ + from astropy.table import Table + datafile="magnitudes_and_probabilities_vlt-fors2_R-SPECIAL.ecsv" + infile = os.path.join(resources.files('zdm'), 'data', 'optical', datafile) + table = Table.read(infile, format='ascii.ecsv') + self.table = table + + def process_rbands(self): + """ + Returns parameters of the host magnitude distribution as a function of redshift + """ + #FRBlist=["FRB20180301A FRB20180916B FRB20190520B FRB20201124A FRB20210410D FRB20121102A FRB20180924B FRB20181112A FRB20190102C FRB20190608B FRB20190611B FRB20190711A FRB20190714A FRB20191001A FRB20200430A FRB20200906A FRB20210117A FRB20210320C FRB20210807D FRB20211127I FRB20211203C FRB20211212A FRB20220105A] + + table = self.table + colnames = table.colnames + # gets FRBs + frblist=[] + for name in colnames: + if name[0:3]=="FRB": + frblist.append(name) + zlist = table["z"] + nz = zlist.size + nfrb = len(frblist) + Rmags = np.zeros([nfrb,nz]) + + for i,frb in enumerate(frblist): + + Rmags[i,:] = table[frb] + + # gets mean and rms + Rbar = np.average(Rmags,axis=0) + Rrms = (np.sum((Rmags - Rbar)**2,axis=0)/(nfrb-1))**0.5 + + # creates cubic spline fits to mean and rms of m_r as a function of z + self.sbar = CubicSpline(zlist,Rbar) + self.srms = CubicSpline(zlist,Rrms) + + #return Rbar,Rrms,zlist,sbar,srms + + def get_pmr_gz(self,mrbins,z): # fsfr must be a self value z: float,fsfr: float): + """ + Returns the p_mr distribution for a given redshift z and sfr fraction f_sfr + + Args: + mrbins (array of floats): list of r-band magnitude bins + z (float): redshift + """ + + mean = self.sbar(z) + rms = self.srms(z) + + deviates = (mrbins-mean)/rms + cprobs = norm.cdf(deviates) + pmr = cprobs[1:] - cprobs[:-1] + + return pmr + + + class loudas_model: """ This class initiates a model based on Nick Loudas's model of @@ -119,6 +217,7 @@ def init_pmr(self,fname,data_dir): data_dir, fname = fname) # zbins represent ranges. We also calculate z-bin centres + self.drmag = rmag_centres[1] - rmag_centres[0] self.zbins = zbins self.nzbins = zbins.size-1 self.czbins = 0.5*(self.zbins[1:] + self.zbins[:-1]) @@ -151,6 +250,8 @@ def init_cubics(self): def get_pmr_gz(self,mrbins,z): # fsfr must be a self value z: float,fsfr: float): """ Returns the p_mr distribution for a given redshift z and sfr fraction f_sfr + Should be defined such that the sum over all mrbins is unity (or less, + if there is a limitation due to range) Args: z (float): redshift @@ -229,12 +330,22 @@ def get_pmr_gz(self,mrbins,z): # fsfr must be a self value z: float,fsfr: float) plt.show() exit() - # distribution foe that redshift assuming sfr weighting + # distribution for that redshift assuming sfr weighting psfr = k1*p_mr_sfr1 + k2*p_mr_sfr2 # mean weighted distribution pmr = pmass*(1.-fsfr) + psfr*fsfr + # normalise by relative bin width - recall, bins should sum to unity + pmr *= (mrbins[1]-mrbins[0])/self.drmag + + # remove negative probabilities - set to zero, and re-normalise + prevsum = np.sum(pmr) + bad = np.where(pmr < 0.)[0] + pmr[bad] = 0. + newsum = np.sum(pmr) + pmr *= prevsum / newsum + return pmr def load_p_mr_distributions(self,data_dir,fname: str = 'p_mr_distributions_dz0.01_z_in_0_1.2.h5') -> tuple: @@ -303,7 +414,7 @@ def give_p_mr_sfr(self,z: float): idx = np.clip(np.searchsorted(self.zbins, z) - 1, 0, n_redshift_bins - 1) return self.p_mr_sfr[idx] - def init_args(self,OpticalState): + def init_args(self,fSFR): """ Initialises prior based on sfr fraction @@ -311,9 +422,10 @@ def init_args(self,OpticalState): opstate: optical model state. Grabs the Loudas parameters from there. """ - self.OpticalState = OpticalState - self.opstate = OpticalState.loudas - self.fsfr = self.opstate.fSFR + # for numerical purposes, fSFR may have to be a vector + if hasattr(fSFR,'__len__'): + fSFR = fSFR[0] + self.fsfr = fSFR def init_priors(self,zlist): """ @@ -409,6 +521,7 @@ def __init__(self,OpticalState=None,verbose=False): self.init_abs_bins() self.init_model_bins() + # could perhaps use init args for this? if self.opstate.AbsPriorMeth==0: # uniform prior in log space of absolute magnitude Absprior = np.full([self.ModelNBins],1./self.NAbsBins) @@ -504,12 +617,15 @@ def get_pmr_gz(self,mrbins,z): with a set of z values. This is all for speedup purposes. Args: - mrbins (np.array, float): array of apparent magnitudes (mr) + mrbins (np.array, float, length N+1): array of apparent magnitudes (mr) over which to calculate p(mr). These act as bins in apparent magnitude mr for histogram purposes, i.e. they are not probabilities *at* mr - zvals (np.ndarray, float): array of redshifts over which + zvals (float): redshifts at which to map absolute to apparent magnitudes. + + Returns: + pmr: probability for each of the bins (length: N) """ # mapping of apparent to absolute magnitude @@ -526,11 +642,12 @@ def get_pmr_gz(self,mrbins,z): smoothf /= np.sum(smoothf) smoothed = np.convolve(hist,smoothf,mode="same") - smoothed=hist + #smoothed=hist. Not sure yet if smoothing is the right thing to do! + pmr = smoothed # # NOTE: these should NOT be re-normalised, since the normalisation reflects # true magnitudes which fall off the apparent magnitude histogram. - return smoothed + return pmr def gauss(self,x,mu=0,sigma=0.1): """ @@ -742,6 +859,9 @@ def init_path_raw_prior_Oi(self,DM,grid): self.prior_DM = DM self.priors = priors + # sets the PATH user function to point to its own + pathpriors.USR_raw_prior_Oi = self.path_raw_prior_Oi + #return priors def get_posterior(self, grid, DM): @@ -1190,14 +1310,3 @@ def plot_frb(name,ralist,declist,plist,opfile): -def load_marnoch_data(): - """ - Loads the Marnoch et al data on r-band magnitudes from FRB hosts - """ - from astropy.table import Table - datafile="magnitudes_and_probabilities_vlt-fors2_R-SPECIAL.ecsv" - infile = os.path.join(resources.files('zdm'), 'data', 'optical', datafile) - table = Table.read(infile, format='ascii.ecsv') - return table - - diff --git a/zdm/optical_numerics.py b/zdm/optical_numerics.py new file mode 100644 index 00000000..713e367e --- /dev/null +++ b/zdm/optical_numerics.py @@ -0,0 +1,234 @@ +""" +Contains files related to numerical optimisation +of FRB host galaxy parameters. Similar to iteration.py +for the grid. +""" + +import numpy as np +from zdm import optical as op +from matplotlib import pyplot as plt + +def function(x,args): + """ + This is a function for input into the scipi.optimize.minimise routine. + + It calculates a set of PATH priors for that model, and then calculates + a test statistic for that set. + + Args: + frblist: list of TNS FRB names + ss: list of surveys in which the FRB may exist + gs: list of grids corresponding to those surveys + model: optical model class which takes arguments x to be minimised. i.e. + the function call model.AbsPrior = x must fully specify the model. + + + """ + + frblist = args[0] + ss = args[1] + gs=args[2] + model=args[3] + + # initialises model to the priors + # generates one per grid, due to possible different zvals + + model.init_args(x) + wrappers = make_wrappers(model,gs) + + NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUprior,PUobs,sumPUprior,sumPUobs = calc_path_priors(frblist,ss,gs,wrappers,verbose=False) + + # we re-normalise the sum of PUs by NFRB + + # prevents infinite plots being created + stat = calculate_goodness_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,sumPUobs,sumPUprior,plotfile=None) + + return stat + +def make_wrappers(model,grids): + """ + returns a list of model wrapper objects for given model and grids + + Args: + model: one of the optical model class objects + grids: list of grid class objects + + Returns: + wrappers: list of wrappers around model, one for each grid + """ + wrappers = [] + for i,g in enumerate(grids): + wrappers.append(op.model_wrapper(model,g.zvals)) + return wrappers + + +def make_cdf(xs,ys,ws,norm = True): + """ + makes a cumulative distribution in terms of + the x-values x, observed values y, and weights w + + """ + cdf = np.zeros([xs.size]) + for i,y in enumerate(ys): + OK = np.where(xs > y)[0] + cdf[OK] += ws[i] + if norm: + cdf /= cdf[-1] + return cdf + + +def calc_path_priors(frblist,ss,gs,wrappers,verbose=True,usemodel=True): + """ + Inner loop. Gets passed model parameters, but assumes everything is + initialsied from there. + + Inputs: + FRBLIST: list of FRBs to retrieve data for + ss: list of surveys modelling those FRBs (searches for FRB in data) + gs: list of zDM grids modelling those surveys + wrappers: list of optical wrapper class objects used to calculate priors on magnitude + verbose (bool): Set to true to generate further output + + Returns: + Number of FRBs fitted + AppMags: list of apparent magnitudes used internally in the model + allMagPriors: summed array of magnitude priors calculated by the model + allObsMags: list of observed magnitudes of candidate hosts + allPOx: list of posterior probabilities calculated by the model + allPU: summed values of unobserved prior + allPUx: summed values of posterior of being unobserved + """ + + NFRB = len(frblist) + + allObsMags = None + allPOx = None + allpriors = None + sumPU = 0. + sumPUx = 0. + allPU = [] + allPUx = [] + nfitted = 0 + + for i,frb in enumerate(frblist): + # interates over the FRBs. "Do FRB" + # P_O is the prior for each galaxy + # P_Ox is the posterior + # P_Ux is the posterior for it being unobserved + # mags is the list of galaxy magnitudes + + # determines if this FRB was seen by the survey, and + # if so, what its DMEG is + for j,s in enumerate(ss): + imatch = op.matchFRB(frb,s) + if imatch is not None: + # this is the survey to be used + g=gs[j] + s = ss[j] + wrapper = wrappers[j] + break + + if imatch is None: + if verbose: + print("Could not find ",frb," in any survey") + continue + + nfitted += 1 + + AppMags = wrapper.AppMags + + DMEG = s.DMEGs[imatch] + # this is where the particular survey comes into it + + # Must be priors on magnitudes for this FRB + wrapper.init_path_raw_prior_Oi(DMEG,g) + + # extracts priors as function of absolute magnitude for this grid and DMEG + MagPriors = wrapper.priors + + mag_limit=26 # might not be correct. TODO! Should be in FRB object + + # calculates unseen prior + if usemodel: + PU = wrapper.estimate_unseen_prior(mag_limit) + else: + PU = 0.1 + MagPriors[:] = 1./len(MagPriors) # log-uniform priors when no model used + + # sets magnitude priors to zero when they are above the magnitude limit + bad = np.where(AppMags > mag_limit)[0] + MagPriors[bad] = 0. + + P_O,P_Ox,P_Ux,ObsMags = op.run_path(frb,usemodel=usemodel,PU = PU) + + ObsMags = np.array(ObsMags) + + if allObsMags is None: + allObsMags = ObsMags + allPOx = P_Ox + allMagPriors = MagPriors + else: + allObsMags = np.append(allObsMags,ObsMags) + allPOx = np.append(allPOx,P_Ox) + allMagPriors += MagPriors + + sumPU += PU + sumPUx += P_Ux + allPU.append(PU) + allPUx.append(P_Ux) + return nfitted,AppMags,allMagPriors,allObsMags,allPOx,allPU,allPUx,sumPU,sumPUx + + +def calculate_goodness_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUobs,PUprior,plotfile=None): + """ + Calculates a ks-like statistics to be proxy for goodness-of-fit + We must set each AppMagPriors to 1.-PUprior at the limiting magnitude for each observation, + and sum the ObsPosteriors to be equal to 1.-PUobs at that magnitude. + Then these are what gets summed. + + This can be readily done by combining all ObsMags and ObsPosteriors into a single long list, + since this should already be correctly normalised. Priors require their own weight. + + Inputs: + AppMags: array listing apparent magnitudes + AppMagPrior: array giving prior on AppMags + ObsMags: list of observed magnitudes + ObsPosteriors: list of posterior values corresponding to ObsMags + PUobs: posterior on unseen probability + PUprior: prior on PU + Plotfile: set to name of output file for comparison plot + + Returns: + k-like statistic of biggest obs/prior difference + """ + + # we calculate a probability using a cumulative distribution + prior_dist = np.cumsum(AppMagPriors) + + # the above is normalised to NFRB. We now divide it by this + # might want to be careful here, and preserve this normalisation + prior_dist /= NFRB #((NFRB-PUprior)/NFRB) / prior_dist[-1] + + + obs_dist = make_cdf(AppMags,ObsMags,ObsPosteriors,norm=False) + + obs_dist /= NFRB + + # we calculate something like the k-statistic. Includes NFRB normalisation + diff = obs_dist - prior_dist + stat = np.max(np.abs(diff)) + + if plotfile is not None: + plt.figure() + plt.xlabel("Apparent magnitude $m_r$") + plt.ylabel("Cumulative host galaxy distribution") + #cx,cy = make_cdf_for_plotting(ObsMags,weights=ObsPosteriors) + plt.plot(AppMags,obs_dist,label="Observed") + plt.plot(AppMags,prior_dist,label="Prior") + plt.legend() + plt.tight_layout() + plt.savefig(plotfile) + plt.close() + + + return stat diff --git a/zdm/scripts/Path/optimise_host_priors.py b/zdm/scripts/Path/optimise_host_priors.py index 279f9728..7a1e744d 100644 --- a/zdm/scripts/Path/optimise_host_priors.py +++ b/zdm/scripts/Path/optimise_host_priors.py @@ -6,7 +6,7 @@ runs PATH using that prior, and tries to get priors to match posteriors. WARNING: this is NOT the optimal method! That would require using -a catalogue of galaxies to sample from to generate fake opotical fields. +a catalogue of galaxies to sample from to generate fake optical fields. But nonetheless, this tests the power of estimating FRB host galaxy contributions using zDM to set priors for apparent magnitudes. @@ -18,8 +18,10 @@ #standard Python imports +import os import numpy as np from matplotlib import pyplot as plt +from scipy.optimize import minimize # imports from the "FRB" series from zdm import optical as opt @@ -28,9 +30,10 @@ from zdm import cosmology as cos from zdm import parameters from zdm import loading +from zdm import optical_numerics as on +# other FRB library imports import astropath.priors as pathpriors -from scipy.optimize import minimize def main(): @@ -55,18 +58,33 @@ def main(): names=['CRAFT_ICS_892','CRAFT_ICS_1300','CRAFT_ICS_1632'] ss,gs = loading.surveys_and_grids(survey_names=names) + modelname = "loudas" + opdir = modelname+"_output/" + if not os.path.exists(opdir): + os.mkdir(opdir) + + # Case of simple host model # Initialisation of model - opt_params = op.Hosts() - opt_params.AbsModelID = 1 - model = opt.host_model(opstate = opt_params) - model.init_zmapping(gs[0].zvals) + # simple host model + if modelname=="simple": + model = opt.simple_host_model() + x0 = model.AbsPrior + elif modelname=="loudas": + #### case of Loudas model + model = opt.loudas_model() + x0 = [0.5] + else: + print("Unrecognised host model ", modelname) - x0 = model.AbsPrior + # initialise aguments to minimisation function args=[frblist,ss,gs,model] Nparams = len(x0) bounds = [(0,1)]*Nparams - result = minimize(function,x0 = x0,args=args,bounds = bounds) + + # "function" is the function that performs the comparison of + # predictions to outcomes. It's where all the magic happens + result = minimize(on.function,x0 = x0,args=args,bounds = bounds) # Recording the current spline best-fit here #x = [0.00000000e+00 0.00000000e+00 7.05155614e-02 8.39235326e-01 @@ -82,17 +100,22 @@ def main(): x = result.x # analyses final result - x /= np.sum(x) - model.AbsPrior = x - model.reinit_model() - outfile = "best_fit_apparent_magnitudes.png" - NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUprior,PUobs,sumPUprior,sumPUobs = calc_path_priors(frblist,ss,gs,model,verbose=False) - stat = calculate_goodness_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,sumPUobs,sumPUprior,plotfile=outfile) + if modelname == "simple": + # renormalise distribution in parameters + x /= np.sum(x) + + # initialises arguments + model.init_args(x) + + outfile = opdir+"best_fit_apparent_magnitudes.png" + wrappers = on.make_wrappers(model,gs) + NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUprior,PUobs,sumPUprior,sumPUobs = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) + stat = on.calculate_goodness_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,sumPUobs,sumPUprior,plotfile=outfile) # calculates the original PATH result - #outfile = "original_fit_apparent_magnitudes.png" - NFRB2,AppMags2,AppMagPriors2,ObsMags2,ObsPosteriors2,PUprior2,PUobs2,sumPUprior2,sumPUobs2 = calc_path_priors(frblist,ss,gs,model,verbose=False,usemodel=False) - #stat = calculate_goodness_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,sumPUobs,sumPUprior,plotfile=outfile) + outfile = opdir+"original_fit_apparent_magnitudes.png" + NFRB2,AppMags2,AppMagPriors2,ObsMags2,ObsPosteriors2,PUprior2,PUobs2,sumPUprior2,sumPUobs2 = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False,usemodel=False) + stat = on.calculate_goodness_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,sumPUobs,sumPUprior,plotfile=outfile) # plots original vs updated posteriors @@ -105,59 +128,45 @@ def main(): plt.scatter(PUobs2,PUobs,label="Unobserved",marker='+') plt.legend() plt.tight_layout() - plt.savefig("Scatter_plot_comparison.png") - plt.close() - - - - # plots final result on absolute magnitudes - plt.figure() - plt.xlabel("Absolute magnitude, $M_r$") - plt.ylabel("$p(M_r)$") - plt.plot(model.AbsMags,model.AbsMagWeights/np.max(model.AbsMagWeights),label="interpolation") - plt.plot(model.ModelBins,x/np.max(x),marker="o",linestyle="",label="Model Parameters") - plt.legend() - plt.tight_layout() - plt.savefig("best_fit_absolute_magnitudes.pdf") + plt.savefig(opdir+"Scatter_plot_comparison.png") plt.close() - -def function(x,args): - """ - function to be minimised - """ - frblist = args[0] - ss = args[1] - gs=args[2] - model=args[3] - - # initialises model to the priors - # technically, there is a redundant normalisation here - model.AbsPrior = x - - NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUprior,PUobs,sumPUprior,sumPUobs = calc_path_priors(frblist,ss,gs,model,verbose=False) - # we re-normalise the sum of PUs by NFRB + ####### Plots that only make sense for specific models ########3 - # prevents infinite plots being created - stat = calculate_goodness_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,sumPUobs,sumPUprior,plotfile=None) - - return stat - - -def make_cdf(xs,ys,ws,norm = True): - """ - makes a cumulative distribution in terms of - the x-values x, observed values y, and weights w + if modelname == "simple": + # plots final result on absolute magnitudes + plt.figure() + plt.xlabel("Absolute magnitude, $M_r$") + plt.ylabel("$p(M_r)$") + plt.plot(model.AbsMags,model.AbsMagWeights/np.max(model.AbsMagWeights),label="interpolation") + plt.plot(model.ModelBins,x/np.max(x),marker="o",linestyle="",label="Model Parameters") + plt.legend() + plt.tight_layout() + plt.savefig(opdir+"best_fit_absolute_magnitudes.pdf") + plt.close() + + if modelname == "loudas": + + NSFR=41 + stats = np.zeros([NSFR]) + SFRs = np.linspace(0,4,NSFR) + for istat,sfr in enumerate(SFRs): + outfile = opdir+"ks_test_sfr_"+str(sfr)+".png" + #outfile = None + model.init_args(sfr) + wrappers = on.make_wrappers(model,gs) + NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUprior,PUobs,sumPUprior,sumPUobs = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) + stat = on.calculate_goodness_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,sumPUobs,sumPUprior,plotfile=outfile) + stats[istat] = stat + outfile = opdir+"scan_sfr.png" + plt.figure() + plt.plot(SFRs,stats,marker="o") + plt.xlabel("$f_{\\rm sfr}$") + plt.ylabel("ks statistic (lower is better)") + plt.tight_layout() + plt.savefig(outfile) + plt.close() - """ - cdf = np.zeros([xs.size]) - for i,y in enumerate(ys): - OK = np.where(xs > y)[0] - cdf[OK] += ws[i] - if norm: - cdf /= cdf[-1] - return cdf - def make_cdf_for_plotting(xvals,weights=None): """ Creates a cumulative distribution function @@ -184,151 +193,8 @@ def make_cdf_for_plotting(xvals,weights=None): cy[2*i+1] = weights[i+1] return cx,cy -def calculate_goodness_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUobs,PUprior,plotfile=None): - """ - Calculates a ks-like statistics to be proxzy for goodness-of-fit - We must set each AppMagPriors to 1.-PUprior at the limiting magnitude for each observation, - and sum the ObsPosteriors to be equal to 1.-PUobs at that magnitude. - Then these are what gets summed. - - This can be readily done by combining all ObsMags and ObsPosteriors into a single long list, - since this should already be correctly normalised. Priors require their own weight. - - Inputs: - AppMags: array listing apparent magnitudes - AppMagPrior: array giving prior on AppMags - ObsMags: list of observed magnitudes - ObsPosteriors: list of posterior values corresponding to ObsMags - PUobs: posterior on unseen probability - PUprior: prior on PU - Plotfile: set to name of output file for comparison plot - - Returns: - k-like statistic of biggest obs/prior difference - """ - - # we calculate a probability using a cumulative distribution - prior_dist = np.cumsum(AppMagPriors) - - # the above is normalised to NFRB. We now divide it by this - # might want to be careful here, and preserve this normalisation - prior_dist /= NFRB #((NFRB-PUprior)/NFRB) / prior_dist[-1] - - - obs_dist = make_cdf(AppMags,ObsMags,ObsPosteriors,norm=False) - - obs_dist /= NFRB - - # we calculate something like the k-statistic. Includes NFRB normalisation - diff = obs_dist - prior_dist - stat = np.max(np.abs(diff)) - - if plotfile is not None: - plt.figure() - plt.xlabel("Apparent magnitude $m_r$") - plt.ylabel("Cumulative host galaxy distribution") - #cx,cy = make_cdf_for_plotting(ObsMags,weights=ObsPosteriors) - plt.plot(AppMags,obs_dist,label="Observed") - plt.plot(AppMags,prior_dist,label="Prior") - plt.legend() - plt.tight_layout() - plt.savefig(plotfile) - plt.close() - - - return stat - -def calc_path_priors(frblist,ss,gs,model,verbose=True,usemodel=True): - """ - Inner loop. Gets passed model parameters, but assumes everything is - initialsied from there. - - Inputs: - FRBLIST: list of FRBs to retrieve data for - ss: list of surveys modelling those FRBs (searches for FRB in data) - gs: list of zDM grids modelling those surveys - model: host_model class object used to calculate priors on magnitude - verbose (bool): guess - - Returns: - Number of FRBs fitted - AppMags: list of apparent magnitudes used internally in the model - allMagPriors: summed array of magnitude priors calculated by the model - allObsMags: list of observed magnitudes of candidate hosts - allPOx: list of posterior probabilities calculated by the model - allPU: summed values of unobserved prior - allPUx: summed values of posterior of being unobserved - """ - - NFRB = len(frblist) - - # we assume here that the model has just had a bunch of parametrs updated - # within it. Must be done once for any fixed zvals. If zvals change, - # then we have another issue - model.reinit_model() - - # do this once per "model" objects - pathpriors.USR_raw_prior_Oi = model.path_raw_prior_Oi - - allObsMags = None - allPOx = None - allpriors = None - AppMags = model.AppMags - sumPU = 0. - sumPUx = 0. - allPU = [] - allPUx = [] - nfitted = 0 + - for i,frb in enumerate(frblist): - # interates over the FRBs. "Do FRB" - # P_O is the prior for each galaxy - # P_Ox is the posterior - # P_Ux is the posterior for it being unobserved - # mags is the list of galaxy magnitudes - - # determines if this FRB was seen by the survey, and - # if so, what its DMEG is - for j,s in enumerate(ss): - imatch = opt.matchFRB(frb,s) - if imatch is not None: - # this is the survey to be used - g=gs[j] - break - - if imatch is None: - if verbose: - print("Could not find ",frb," in any survey") - continue - - nfitted += 1 - - DMEG = s.DMEGs[imatch] - # this is where the particular survey comes into it - MagPriors = model.init_path_raw_prior_Oi(DMEG,g) - mag_limit=26 # might not be correct - PU = model.estimate_unseen_prior(mag_limit) - bad = np.where(AppMags > mag_limit)[0] - MagPriors[bad] = 0. - - P_O,P_Ox,P_Ux,ObsMags = opt.run_path(frb,model,usemodel=usemodel,PU = PU) - - ObsMags = np.array(ObsMags) - - if allObsMags is None: - allObsMags = ObsMags - allPOx = P_Ox - allMagPriors = MagPriors - else: - allObsMags = np.append(allObsMags,ObsMags) - allPOx = np.append(allPOx,P_Ox) - allMagPriors += MagPriors - - sumPU += PU - sumPUx += P_Ux - allPU.append(PU) - allPUx.append(P_Ux) - return nfitted,AppMags,allMagPriors,allObsMags,allPOx,allPU,allPUx,sumPU,sumPUx main() diff --git a/zdm/scripts/Path/plot_host_models.py b/zdm/scripts/Path/plot_host_models.py index 8c7256e3..712ee364 100644 --- a/zdm/scripts/Path/plot_host_models.py +++ b/zdm/scripts/Path/plot_host_models.py @@ -1,14 +1,16 @@ """ Script showing how to load different FRB host models. -It does this for both my simple naive model, and Nick's -model based on mass- or sfr-weightings. +It does this for both my simple naive model, and Nick Loudas's +model based on mass- or sfr-weightings, and Lachlan's +evolution of galaxy spectra. We then evaluate P(O|x) for CRAFT FRBs in the CRAFT 1300 MHz survey """ #standard Python imports +import os import numpy as np from matplotlib import pyplot as plt @@ -29,7 +31,8 @@ def calc_path_priors(): """ opdir = "Plots/" - + if not os.path.exists(opdir): + os.mkdir(opdir) ##### performs the following calculations for the below combinations ###### # loads a default optical state. @@ -87,18 +90,17 @@ def calc_path_priors(): plt.close() - # set up basic histogram of p(mr) distribution mrbins = np.linspace(0,30,301) mrvals=(mrbins[:-1]+mrbins[1:])/2. - + model3 = opt.marnoch_model() ######### Plots apparent mag distribution for all models as function of z ####### styles=["-","--",":","-."] plt.figure() - flist=[0,0.5,1.] + flist=[0,1.] for z in [0.1,0.5,2]: @@ -109,12 +111,16 @@ def calc_path_priors(): # Loudas model dependencies for i,fsfr in enumerate(flist): - opstate2.loudas.fSFR = fsfr # mass-dependent - model2.init_args(opstate2) + model2.init_args(fsfr) pmr = model2.get_pmr_gz(mrbins,z) pmr /= np.sum(pmr) - plt.plot(mrvals,pmr,label="z = "+str(z)+", $f_{\\rm sfr}$ = "+str(fsfr),linestyle=styles[i+1],color=plt.gca().lines[-1].get_color()) - + plt.plot(mrvals,pmr,label = "z = "+str(z)+", $f_{\\rm sfr}$ = "+str(fsfr), + linestyle=styles[i+1],color=plt.gca().lines[-1].get_color()) + + pmr = model3.get_pmr_gz(mrbins,z) + plt.plot(mrvals,pmr,label = "Marnoch: z = "+str(z),linestyle=styles[3], + color=plt.gca().lines[-1].get_color()) + plt.xlabel("Optical magnitude $m_r$") plt.ylabel("p(m_r|z)") plt.tight_layout() @@ -123,7 +129,6 @@ def calc_path_priors(): plt.close() - ############################################################################ #Load a grid. We'll only load data from the ICS 1300 survey. Just to use it # as an example calculation for particular DMs @@ -137,6 +142,7 @@ def calc_path_priors(): # wrapper around the optical model. For returning p(m_r|DM) wrapper1 = opt.model_wrapper(model1,g.zvals) # simple wrapper2 = opt.model_wrapper(model2,g.zvals) # loudas with fsfr=0 + wrapper3 = opt.model_wrapper(model3,g.zvals) # loudas with fsfr=0 # do this once per "model" objects #pathpriors.USR_raw_prior_Oi = wrapper1.path_raw_prior_Oi @@ -154,25 +160,30 @@ def calc_path_priors(): # we first change the underlying state # then we initialise the model # then we re-init the wrapper. - opstate2.loudas.fSFR=0. - model2.init_args(opstate2) + fSFR=0. + model2.init_args(fSFR) wrapper2.init_zmapping(g.zvals) wrapper2.init_path_raw_prior_Oi(DM,g) - plt.plot(wrapper2.AppMags,wrapper2.priors,label="DM = "+str(DM)+", $f_{\\rm sfr}$ = 0.0", + plt.plot(wrapper2.AppMags,wrapper2.priors,label="DM = "+str(DM)+", Loudas: $f_{\\rm sfr}$ = 0.0", linestyle=styles[1],color=plt.gca().lines[-1].get_color()) - opstate2.loudas.fSFR=1.0 - model2.init_args(opstate2) + fSFR=1.0 + model2.init_args(fSFR) wrapper2.init_zmapping(g.zvals) wrapper2.init_path_raw_prior_Oi(DM,g) - plt.plot(wrapper2.AppMags,wrapper2.priors,label="DM = "+str(DM)+", $f_{\\rm sfr}$ = 1.0", + plt.plot(wrapper2.AppMags,wrapper2.priors,label="DM = "+str(DM)+", Loudas: $f_{\\rm sfr}$ = 1.0", linestyle=styles[2],color=plt.gca().lines[-1].get_color()) - + + wrapper3.init_zmapping(g.zvals) + wrapper3.init_path_raw_prior_Oi(DM,g) + plt.plot(wrapper3.AppMags,wrapper3.priors,label="DM = "+str(DM)+", Marnoch", + linestyle=styles[2],color=plt.gca().lines[-1].get_color()) + plt.xlabel("Absolute magnitude $M_r$") plt.ylabel("$p(M_r)$") plt.legend() plt.tight_layout() - plt.savefig(opdir+"all_models_mag_priors_dm.png") + plt.savefig(opdir+"all_model_mag_priors_dm.png") plt.close() # do this only for a particular FRB @@ -180,11 +191,11 @@ def calc_path_priors(): #AppMagPriors,pz = model.get_posterior(g,DMlist) - maglist = [None,None,None,None] - allPOx = [None,None,None,None] + maglist = [None,None,None,None,None] + allPOx = [None,None,None,None,None] - labels=["Orig","Simple","Mass-weighted","SFR weighted"] - markers=["x","+","s","o"] + labels=["Orig","Simple","Mass-weighted","SFR weighted","Marnoch"] + markers=["x","+","s","o","v"] for i,frb in enumerate(frblist): # interates over the FRBs. "Do FRB" @@ -240,8 +251,8 @@ def calc_path_priors(): # loudas fsfr = 0.0 (i.e., mass weighted) - opstate2.loudas.fSFR=0.0 - model2.init_args(opstate2) + fSFR=0.0 + model2.init_args(fSFR) wrapper2.init_zmapping(g.zvals) wrapper2.init_path_raw_prior_Oi(DMEG,g) PU3 = wrapper2.estimate_unseen_prior(mag_limit=26) # might not be correct @@ -258,8 +269,8 @@ def calc_path_priors(): # loudas fsfr = 1.0 - opstate2.loudas.fSFR=1.0 - model2.init_args(opstate2) + fSFR=1.0 + model2.init_args(fSFR) wrapper2.init_zmapping(g.zvals) wrapper2.init_path_raw_prior_Oi(DMEG,g) PU4 = wrapper2.estimate_unseen_prior(mag_limit=26) # might not be correct limit @@ -274,12 +285,27 @@ def calc_path_priors(): maglist[3] = np.append(maglist[3],mags4) allPOx[3] = np.append(allPOx[3],P_Ox4) - + # Marnoch model + wrapper3.init_zmapping(g.zvals) + wrapper3.init_path_raw_prior_Oi(DMEG,g) + PU5 = wrapper3.estimate_unseen_prior(mag_limit=26) # might not be correct limit + pathpriors.USR_raw_prior_Oi = wrapper3.path_raw_prior_Oi + P_O5,P_Ox5,P_Ux5,mags5 = opt.run_path(frb,usemodel=True,PU = PU5) + + # record this info + if maglist[4] is None: + maglist[4] = mags5 + allPOx[4] = P_Ox5 + else: + maglist[4] = np.append(maglist[4],mags5) + allPOx[4] = np.append(allPOx[4],P_Ox5) + + # scatter plot of old vs new priors plt.figure() plt.xlabel("$P(O|x)$ (original)") plt.ylabel("$P(O|x)$ (new)") - for j in np.arange(1,4,1): + for j in np.arange(1,5,1): plt.scatter(allPOx[0],allPOx[j],label=labels[j],marker=markers[j]) plt.legend() plt.tight_layout() From a6060642150e583a2298eb24f4747c10b0395747 Mon Sep 17 00:00:00 2001 From: Clancy James Date: Thu, 8 Jan 2026 10:19:31 +0800 Subject: [PATCH 09/35] Found a bug in states.py, added plotting scripts for lsst paper, updated scripts in PATH scripts directory --- papers/lsst/Data/craft_ics_hosts.csv | 32 +++++ papers/lsst/Data/dsa_hosts.csv | 31 ++++ papers/lsst/Data/meerkat_mr.txt | 25 ++++ papers/lsst/galaxies.py | 75 ++++++++++ papers/lsst/make_host_z_mag_plot.py | 151 +++++++++++++++++++ papers/lsst/meerkat_mr.txt | 13 -- papers/lsst/sim_pz.py | 176 ++++++++++++++++++----- zdm/optical.py | 13 +- zdm/optical_numerics.py | 30 +++- zdm/scripts/Path/optimise_host_priors.py | 13 +- zdm/scripts/Path/plot_host_models.py | 22 ++- zdm/states.py | 3 +- 12 files changed, 521 insertions(+), 63 deletions(-) create mode 100644 papers/lsst/Data/craft_ics_hosts.csv create mode 100644 papers/lsst/Data/dsa_hosts.csv create mode 100644 papers/lsst/Data/meerkat_mr.txt create mode 100644 papers/lsst/galaxies.py create mode 100644 papers/lsst/make_host_z_mag_plot.py delete mode 100644 papers/lsst/meerkat_mr.txt diff --git a/papers/lsst/Data/craft_ics_hosts.csv b/papers/lsst/Data/craft_ics_hosts.csv new file mode 100644 index 00000000..cde8de12 --- /dev/null +++ b/papers/lsst/Data/craft_ics_hosts.csv @@ -0,0 +1,32 @@ +TNS,z,mr,pmr +20180924B,0.3214,21.32 +20181112A,0.4755,21.49 +20190102C,0.29,20.73 +20190608B,0.1178,17.15 +20190611B,0.378,22.35 +20190711A,0.522,22.93 +20190714A,0.2365,19.47 +20191001A,0.23,17.82 +20191228B,0.2432,21.92 +20200430A,0.161,21.18 +20200906A,0.3688,20.70 +20210117A,0.214,22.95 +20210320C,0.28,19.23 +20210807D,0.1293,17.35 +20211127I,0.046946,15.38 +20211203C,0.3439,20.29 +20211212A,0.0707,16.21 +20220105A,0.2785,21.53 +20220501C,0.381,20.57 +20220610A,1.015,23.99 +20220725A,0.1926,17.83 +20220918A,0.491,23.6 +20221106A,0.2044,18.34 +20230526A,0.157,21.15 +20230708A,0.105,22.73 +20230902A,0.3619,21.52 +20231226A,0.156,19.01 +20240201A,0.042729,16.97 +20240210A,0.023686,15.13 +20240304A,0.2423,21.08 +20240310A,0.127,20.16 diff --git a/papers/lsst/Data/dsa_hosts.csv b/papers/lsst/Data/dsa_hosts.csv new file mode 100644 index 00000000..5abf1c48 --- /dev/null +++ b/papers/lsst/Data/dsa_hosts.csv @@ -0,0 +1,31 @@ +z,mr,phost +0.0112,13.25,0.99 +0.0368,15.41,0.99 +0.0433,16.15,0.97 +0.0894,16.51,0.97 +0.0939,19.10,0.99 +0.1139,20.20,0.97 +0.1270,19.94,0.95 +0.1582,19.43,0.98 +0.2395,18.81,0.99 +0.2414,20.74,0.99 +0.2481,20.45,0.98 +0.2505,21.38,0.99 +0.2621,18.53,0.90 +0.2706,19.48,0.97 +0.2764,19.01,0.94 +0.2847,19.64,0.99 +0.300,19.91,0.99 +0.3015,21.86,0.99 +0.3270,19.97,0.99 +0.3510,20.80,0.56 +0.3619,20.95,0.99 +0.3714,23.35,0.63 +0.4012,23.00,0.99 +0.4525,21.36,0.99 +0.4780,21.20,0.97 +0.5310,21.38,0.42 +0.5422,22.84,0.62 +0.5530,22.61,0.99 +0.6214,21.22,0.97 +0.9750,23.17,0.92 diff --git a/papers/lsst/Data/meerkat_mr.txt b/papers/lsst/Data/meerkat_mr.txt new file mode 100644 index 00000000..74ddaf7d --- /dev/null +++ b/papers/lsst/Data/meerkat_mr.txt @@ -0,0 +1,25 @@ +# r-band magnitudes of FRB hosts from Table D1 of Pastor-Morales et al +# https://arxiv.org/pdf/2507.05982 +# -ve z means photometric redshift +# 9999 means no data +# loc=0 is incoherent detection, 1 is coherent +# FRB DMX zspec mr loc w +20220222 974 0.853 23.86 1 1 +20220224 1055 0.6271 21.63 0 1 +20230125 459 0.3265 22.12 1 1 +20230306 636 9999 9999 1 1 +20230413 1461 9999 9999 1 1 +20230503 350 -0.32 20.11 0 1 +20230613 427 0.3923 20.132 1 1 +20230808 590 0.3472 22.3 1 0.51 +20230808 590 0.3472 22.9 1 0.49 +20230814 249 9999 22.03 0 1 +20230827 1364 9999 9999 1 1 +20230907 973 0.4638 19.83 0 1 +20231007 2597 9999 9999 0 1 +20231010 370 -0.61 21.24 1 0.52 +20231010 370 -0.84 22.52 1 0.47 +20231020 892 0.4775 21.81 0 1 +20231204 1700 9999 9999 1 1 +20231210 662 -0.5 21.19 1 0.81 +20231210 662 -1.37 23.9 1 0.148 diff --git a/papers/lsst/galaxies.py b/papers/lsst/galaxies.py new file mode 100644 index 00000000..e19be4f0 --- /dev/null +++ b/papers/lsst/galaxies.py @@ -0,0 +1,75 @@ +""" +File containing routines to read in host galaxy data +""" + +import numpy as np +import pandas as pd + +def read_meerkat(): + """ + returns z and mr data from Pastor-Morales et al + https://arxiv.org/pdf/2507.05982 + Detection method provided in private communication (Pastor-Morales) + """ + + data=np.loadtxt("Data/meerkat_mr.txt",comments='#') + z=data[:,2] + mr = data[:,3] + loc = data[:,4] # 1 is coherent beam, 0 incoherent only + z = np.abs(z) # -ve is + w = data[:,5] #PO|x + + # removes incoherent sum data + good = np.where(loc==1)[0] + z=z[good] + loc=loc[good] + mr=mr[good] + w = w[good] + + # removes missing data + good = np.where(z != 9999) + z = z[good] + loc=loc[good] + mr=mr[good] + w=w[good] + + return z,mr,w + +def convert_craft(): + """ + CRAFT ICS data + """ + + import pandas as pd + DF = pd.read_csv("Data/CRAFT_ICS_HTR_Catalogue1.csv") + + DF2 = pd.DataFrame(DF["TNS"]) + DF2["z"] = DF["Z"] + DF2.to_csv("Data/temp_craft_hosts.csv",index=False) + +def read_craft(): + """ + CRAFT ICS data + """ + + DF = pd.read_csv("Data/craft_ics_hosts.csv") + + z = np.array(DF["z"]) + mr = np.array(DF["mr"]) + nfrb = len(mr) + w = np.full([nfrb],1.) # artificial, but all are highy confidence + return z,mr,w + + + +def read_dsa(): + """ + Reads in DSA data from sharma et al + """ + DF = pd.read_csv("Data/dsa_hosts.csv") + + z = np.array(DF["z"]) + mr = np.array(DF["mr"]) + nfrb = len(mr) + w = np.array(DF["phost"]) # only gives most likely hosts + return z,mr,w diff --git a/papers/lsst/make_host_z_mag_plot.py b/papers/lsst/make_host_z_mag_plot.py new file mode 100644 index 00000000..6f5e5e15 --- /dev/null +++ b/papers/lsst/make_host_z_mag_plot.py @@ -0,0 +1,151 @@ +""" +Plots the expected magnitude-z relation for various FRB host galaxy models + +Adds points corresponding to known hosts + +""" + +#standard Python imports +import os +import numpy as np +from matplotlib import pyplot as plt + +import galaxies as g + +# imports from the "FRB" series +from zdm import optical as opt +from zdm import optical_params as op +from zdm import loading +from zdm import cosmology as cos +from zdm import parameters +from zdm import loading +import galaxies as g +import astropath.priors as pathpriors +import matplotlib + +defaultsize=14 +ds=4 +font = {'family' : 'Helvetica', + 'weight' : 'normal', + 'size' : defaultsize} +matplotlib.rc('font', **font) + +def make_zmr_plots(): + """ + Loops over all ICS FRBs + """ + + # loops over different FRB host models + opdir="zmr/" + if not os.path.exists(opdir): + os.mkdir(opdir) + + # model 1: simple model + model1 = opt.simple_host_model() + absprior = [0,0,0.0705, 0.839, 0.328, 0.001,0,0,0,0] + model1.init_args(absprior) + + + # this is from an initial estimate. Currently, no way to enter this into the opstate. To do. + + #loudas model with SFR=0 + model2=opt.loudas_model() + model2.init_args(0.) + + # loudas model with SFR=1 + model3=opt.loudas_model() + model3.init_args(1.) + + # model from Lachlan + model4=opt.marnoch_model() + + models=[model1,model2,model3,model4] + labels=["simple","sfr0","sfr1","marnoch"] + + for i,model in enumerate(models): + opfile = opdir+labels[i]+"_zmr.png" + make_host_plot(model,opfile) + + +def make_host_plot(model,opfile): + """ + generates a plot showing the magnitude and redshift of a bunch of FRB host galaxies + + Args: + model: optical model class instance + opfile: string labelling the plotfile + """ + + nz=200 + zmax=2 + zmin = zmax/nz + zvals = np.linspace(zmin,zmax,nz) + mrbins = np.linspace(0,40,401) + mrvals = (mrbins[1:] + mrbins[:-1])/2. + + medians = np.zeros([nz]) + sig1ds = np.zeros([nz]) + sig1us = np.zeros([nz]) + sig2ds = np.zeros([nz]) + sig2us = np.zeros([nz]) + + for i,z in enumerate(zvals): + pmr = model.get_pmr_gz(mrbins,z) + + cpmr = np.cumsum(pmr) + cpmr /= cpmr[-1] + median = np.where(cpmr > 0.5)[0][0] + sig1d = np.where(cpmr < 0.165)[0][-1] + sig1u = np.where(cpmr > 0.835)[0][0] + sig2d = np.where(cpmr < 0.025)[0][-1] + sig2u = np.where(cpmr > 0.975)[0][0] + + medians[i] = mrvals[median] + sig1ds[i] = mrvals[sig1d] + sig1us[i] = mrvals[sig1u] + sig2ds[i] = mrvals[sig2d] + sig2us[i] = mrvals[sig2u] + + plt.figure() + plt.plot(zvals,medians,linestyle="-",color="red",label="Mean") + plt.plot(zvals,sig1ds,linestyle="--",color=plt.gca().lines[-1].get_color(),label="67% C.I.") + plt.plot(zvals,sig1us,linestyle="--",color=plt.gca().lines[-1].get_color()) + plt.plot(zvals,sig2ds,linestyle=":",color=plt.gca().lines[-1].get_color(),label="95% C.I.") + plt.plot(zvals,sig2us,linestyle=":",color=plt.gca().lines[-1].get_color()) + plt.xlabel("z") + plt.ylabel("$m_r$") + + z,mr,w = g.read_craft() + OK = np.where(w>= 0.5)[0] + plt.scatter(z[OK],mr[OK],marker='d',label="CRAFT ICS",s=20) + + z,mr,w = g.read_meerkat() + OK = np.where(w>= 0.5)[0] + plt.scatter(z[OK],mr[OK],marker='+',label="MeerTRAP coherent",s=20) + + z,mr,w = g.read_dsa() + OK = np.where(w>= 0.5)[0] + plt.scatter(z[OK],mr[OK],marker='o',label="DSA",s=20) + + plt.ylim(10,30) + plt.xlim(0,1.5) + + Rlim1=24.7 + Rlim2=27.5 + plt.plot([0,1.5],[Rlim1,Rlim1],linestyle=":",color="black") + plt.plot([0,1.5],[Rlim2,Rlim2],linestyle=":",color="black") + plt.text(0.1,Rlim1+0.2,"$m_r^{\\rm lim}=$"+str(Rlim1)) + plt.text(0.1,Rlim2+0.2,"$m_r^{\\rm lim}=$"+str(Rlim2)) + + plt.legend() + plt.tight_layout() + plt.savefig(opfile) + plt.close() + + +if __name__ == "__main__": + + make_zmr_plots() + + + diff --git a/papers/lsst/meerkat_mr.txt b/papers/lsst/meerkat_mr.txt deleted file mode 100644 index 327c8558..00000000 --- a/papers/lsst/meerkat_mr.txt +++ /dev/null @@ -1,13 +0,0 @@ -# r-band magnitudes of FRB hosts from Table B1 of Pastor-Morales et al https://arxiv.org/pdf/2507.05982 -# FRB zspec mr loc -20220222 0.853 23.86 -20220224 0.6271 21.63 -20230125 0.3265 22.12 -20230503 -0.32 20.11 -20230613 0.3923 20.132 -20230827 0 22.03 -20230827 0 25.7 -20230907 0.4638 19.83 -20231010 -0.61 21.24 -20231020 0.4775 0.46 -20231210 -0.5 21.19 diff --git a/papers/lsst/sim_pz.py b/papers/lsst/sim_pz.py index f3051c0b..27a2cfba 100644 --- a/papers/lsst/sim_pz.py +++ b/papers/lsst/sim_pz.py @@ -39,33 +39,40 @@ def main(opdir="Data/"): - meerkat_z,meerkat_mr = read_meerkat() + plotdir="Plots/" - Load=True + meerkat_z,meerkat_mr,meerkat_w = read_meerkat() + + # we should re-do this shortly. + Load=False repeaters=False + Test=False # do this for very simplified data + Scat=False # do not use updated scattering model Rlim0 = 19.8 # existing magnitude limits Rlim1 = 24.7 Rlim2 = 27.5 names=['CRAFT_CRACO_1300','MeerTRAPcoherent','SKA_mid'] - labels=["ASKAP CRACO", "MeerKAT/SKA-Mid","SKA-Mid"] + labels=["ASKAP CRACO", "MeerKAT","SKA-Mid"] prefixes=["CRACO","MeerTRAP","SKA_Mid"] linestyles = ["-","--",":"] imax=2 # because SKA and mid are so similar + if not os.path.exists(plotdir): + os.mkdir(plotdir) if not os.path.exists(opdir): os.mkdir(opdir) - + Rs,Rrmss,Rzvals,sbar,srms = process_rbands() plot_R(Rs,Rrmss,Rzvals,sbar,srms,opdir,Rlim1,Rlim2) - + if not Load: #gs,ss = get_surveys_grids(names,opdir,repeaters=True,Test=False) - ss,gs = get_surveys_grids(names,opdir,repeaters=False,Test=False) + ss,gs = get_surveys_grids(names,opdir,repeaters=repeaters,Test=Test,Scat=Scat) - plot_efficiencies(gs,ss,opdir,prefixes) + plot_efficiencies(gs,ss,opdir,prefixes,Test,Scat) plot_beams(ss,labels,opdir) # plots telescope efficiencies at z=0 @@ -125,15 +132,19 @@ def main(opdir="Data/"): plt.figure() plt.xlabel("z") - plt.ylabel("$f_{m_r}$") - plt.plot(zvals,fz1,label="$f_{24.7}$") - plt.plot(zvals,fz2,label="$f_{27.5}$") + plt.ylabel("fraction visible") + plt.plot(zvals,fz1,label="$m_{r}^{\\rm lim}=24.7$") + plt.plot(zvals,fz2,label="$m_{r}^{\\rm lim}=27.5$",linestyle="--") + plt.ylim(0,1) + plt.xlim(0,6) plt.legend() plt.tight_layout() - plt.savefig(opdir+"fraction_visible.png") + plt.savefig(plotdir+"fraction_visible.png") plt.close() - DPplot(zvals,[fz1],["$m_r = 24.7$"],opdir + "DP_fraction_visible.png",color="orange") + + + DPplot(zvals,[fz1],["$m_r = 24.7$"],plotdir + "DP_fraction_visible.png",color="orange") ####### p(z) plot ##### plt.figure() @@ -146,7 +157,7 @@ def main(opdir="Data/"): plt.xlabel("$m_r$") plt.ylabel("$p(m_r)$ [a.u.]") ax2 = plt.gca() - + imax=2 for i,prefix in enumerate(prefixes): if i==imax: break @@ -187,7 +198,7 @@ def main(opdir="Data/"): print(i,"Norm is ",norm) plt.plot(zvals,pz/norm,label=labels[i],linestyle="-") - plt.plot(zvals,pz0/norm,linestyle="-.",color=plt.gca().lines[-1].get_color()) + #plt.plot(zvals,pz0/norm,linestyle="-.",color=plt.gca().lines[-1].get_color()) plt.plot(zvals,pz1/norm,linestyle="--",color=plt.gca().lines[-1].get_color()) plt.plot(zvals,pz2/norm,linestyle=":",color=plt.gca().lines[-1].get_color()) @@ -203,15 +214,19 @@ def main(opdir="Data/"): plt.sca(ax2) norm = np.max(mag_hist) - plt.plot(Rbars,mag_hist/norm,label=labels[i]) + plt.plot(Rbars,mag_hist/norm,label=labels[i],linestyle=linestyles[i]) + + if prefix == "MeerTRAP": + mtmh = mag_hist + mtpz = pz plt.sca(ax1) plt.ylim(0,1.02) - plt.xlim(0,6) + plt.xlim(0,5) plt.legend() plt.tight_layout() - plt.savefig(opdir+"lsst_pz.png") + plt.savefig(plotdir+"lsst_pz.png") plt.close() plt.sca(ax2) @@ -220,25 +235,83 @@ def main(opdir="Data/"): #meerkat_z,meerkat_mr mrbins=np.linspace(10,30,21) - weights = np.full([11],0.25) + nfrb = len(meerkat_mr) + - plt.hist(meerkat_mr,bins=mrbins,weights=weights,label="MK 2023 data") plt.plot([Rlim1,Rlim1],[0,1],linestyle=":",color="black") plt.plot([Rlim2,Rlim2],[0,1],linestyle=":",color="black") - plt.text(Rlim1+0.1,0.1,"$m_r=$"+str(Rlim1),rotation=90) - plt.text(Rlim2+0.1,0.1,"$m_r=$"+str(Rlim2),rotation=90) + plt.text(Rlim1-1.5,0.1,"$m_r^{\\rm lim}=$"+str(Rlim1),rotation=90) + plt.text(Rlim2-1.5,0.1,"$m_r^{\\rm lim}=$"+str(Rlim2),rotation=90) + #plt.legend(loc="upper left") + plt.legend(fontsize=14) + plt.tight_layout() + plt.savefig(plotdir+"lsst_pR.png") + + + plt.hist(meerkat_mr,bins=mrbins,weights=meerkat_w/4.,label="MK 2023 data") + plt.legend() + plt.tight_layout() + plt.savefig(plotdir+"lsst_pR_w_hist.png") + plt.close() + + ########## MeerKAT comparisons ########## + ####### magnitude ####### + # does cumulative histogram, and compares to expected + mtcs = np.cumsum(mtmh) + mtcs /= mtcs[-1] + from zdm import optical_numerics as on + cdf = on.make_cdf(Rbars,meerkat_mr,meerkat_w,norm=False) + # normalsie by fraction of FRBs actually studied + + cdf *= np.sum(meerkat_w)/(10.*cdf[-1]) # should get about 6 of 10 FRBs + + maxmr = np.max(meerkat_mr) + OK = np.where(Rbars <= 24)[0] + + plt.figure() + plt.xlabel("$m_r$") + plt.ylabel("cdf$(m_r)$") + plt.ylim(0,1) + plt.xlim(15,30) + plt.plot(Rbars,mtcs,label="Prediction",linestyle="--") + plt.plot(Rbars[OK],cdf[OK],label="Observations",linestyle="-") plt.legend() plt.tight_layout() - plt.savefig(opdir+"lsst_pR.png") + plt.savefig(plotdir+"meerkat_mr_comparison.png") plt.close() - DPplot(zvals,[pz,pz1],["all FRBs","LSST"],opdir + "DP_pz.png",color="orange",legend=False) + ########## redshift ######## + mtcs = np.cumsum(mtpz) + mtcs /= mtcs[-1] + + cdf = on.make_cdf(zvals,meerkat_z,meerkat_w,norm=False) + # normalsie by fraction of FRBs actually studied + + cdf *= np.sum(meerkat_w)/(10.*cdf[-1]) # should get about 6 of 10 FRBs + + maxz = np.max(meerkat_z) + + OK = np.where(zvals <= maxz+0.1)[0] + + plt.figure() + plt.xlabel("$z$") + plt.ylabel("cdf$(z)$") + plt.ylim(0,1) + plt.xlim(0,3) + plt.plot(zvals,mtcs,label="Prediction",linestyle="--") + plt.plot(zvals[OK],cdf[OK],label="Observations",linestyle="-") + plt.legend() + plt.tight_layout() + plt.savefig(plotdir+"meerkat_z_comparison.png") + plt.close() + + DPplot(zvals,[pz,pz1],["all FRBs","LSST"],plotdir + "DP_pz.png",color="orange",legend=False) DPplot(zvals,[pz,pz1,pz0],["all FRBs","LSST","Now"],opdir + "DP_pz0.png",color="orange",legend=False) DPplot(zvals,[pz1,pz0],["LSST","Now"],opdir + "DP_lsst_vs_now.png",color="orange",legend=False) - + def DPplot(zvals,yvals,labels,outfile,color="orange",legend=True): fig = plt.figure() @@ -301,13 +374,31 @@ def read_meerkat(): """ returns z and mr data from Pastor-Morales et al https://arxiv.org/pdf/2507.05982 + Detection method provided in private communication (Pastor-Morales) """ - data=np.loadtxt("meerkat_mr.txt",comments='#') - z=data[:,1] - mr = data[:,2] - z = np.abs(z) - return z,mr + data=np.loadtxt("Data/meerkat_mr.txt",comments='#') + z=data[:,2] + mr = data[:,3] + loc = data[:,4] # 1 is coherent beam, 0 incoherent only + z = np.abs(z) # -ve is + w = data[:,5] #PO|x + + # removes incoherent sum data + good = np.where(loc==1)[0] + z=z[good] + loc=loc[good] + mr=mr[good] + w = w[good] + + # removes missing data + good = np.where(z != 9999) + z = z[good] + loc=loc[good] + mr=mr[good] + w=w[good] + + return z,mr,w def plot_R(Rbars,Rrmss,Rzvals,sbar,srms,opdir,Rlim1,Rlim2): # plot of mean and rms from Gaussian assumption @@ -329,13 +420,21 @@ def plot_R(Rbars,Rrmss,Rzvals,sbar,srms,opdir,Rlim1,Rlim2): plt.close() -def plot_efficiencies(gs,ss,opdir,prefixes): +def plot_efficiencies(gs,ss,opdir,prefixes,Test=False,Scat=False): + """ + Generates a plot of efficiencies at the 0th zbin. Or, for all zbins, + if we are doing a test + """ + for i,s in enumerate(ss): plt.figure() g=gs[i] for j,w in enumerate(s.wlist): - plt.plot(g.dmvals,s.efficiencies[j,0,:],label="w="+str(w)[0:5]) # at z=0 + if Scat: + plt.plot(g.dmvals,s.efficiencies[j,0,:],label="w="+str(w)[0:5]) # at z=0 + else: + plt.plot(g.dmvals,s.efficiencies[j,:],label="w="+str(w)[0:5]) plt.xlabel("DM") plt.ylabel("$\\epsilon$") plt.yscale("log") @@ -345,13 +444,16 @@ def plot_efficiencies(gs,ss,opdir,prefixes): plt.savefig(opdir+prefixes[i]+"_efficiencies.png") plt.close() -def get_surveys_grids(names,opdir,repeaters=True,Test=False): +def get_surveys_grids(names,opdir,repeaters=True,Test=False,Scat=False): # approximate best-fit values from recent analysis # load states from Hoffman et al 2025 # use b or d for rep - state = states.load_state("HoffmannEmin25",scat="updated",rep='b') + if Scat: + state = states.load_state("HoffmannHalo25",scat="updated",rep='b') + else: + state = states.load_state("HoffmannHalo25",rep='b') # artificially add repeater data - we can't actually know this, # because we don't have time per field. Just using one day for now @@ -383,7 +485,7 @@ def get_surveys_grids(names,opdir,repeaters=True,Test=False): nz=50 dmmax=4000 zmax=4 - survey_dict["WMETHOD"] = 2 + else: ndm=1400 nz=600 @@ -391,8 +493,12 @@ def get_surveys_grids(names,opdir,repeaters=True,Test=False): zmax=6 # uses redshift-dependent scattering. This takes longer # - by a factor of a few! - survey_dict["WMETHOD"] = 3 + #survey_dict["Wmethod"] = 3 + if Scat: + survey_dict["Wmethod"] = 3 + else: + survey_dict["Wmethod"] = 2 ss,gs = loading.surveys_and_grids(survey_names=names,repeaters=repeaters,init_state=state, sdir=sdir,survey_dict=survey_dict,nz=nz,zmax=zmax,ndm=ndm,dmmax=dmmax) return ss,gs diff --git a/zdm/optical.py b/zdm/optical.py index 7250b3fb..1ca52f8e 100644 --- a/zdm/optical.py +++ b/zdm/optical.py @@ -373,7 +373,7 @@ def load_p_mr_distributions(self,data_dir,fname: str = 'p_mr_distributions_dz0.0 rmag_centers = np.array(hf['rmag_centers']) p_mr_sfr = np.array(hf['p_mr_sfr']) p_mr_mass = np.array(hf['p_mr_mass']) - + # normalise these probabilities such that the bins sum to unity p_mr_sfr = (p_mr_sfr.T / np.sum(p_mr_sfr,axis=1)).T p_mr_mass = (p_mr_mass.T / np.sum(p_mr_mass,axis=1)).T @@ -981,6 +981,17 @@ def path_raw_prior_Oi(self,mags,ang_sizes,Sigma_ms): ################# Useful functions not associated with a class ######### + +def load_marnoch_data(): + """ + Loads the Marnoch et al data on r-band magnitudes from FRB hosts + """ + from astropy.table import Table + datafile="magnitudes_and_probabilities_vlt-fors2_R-SPECIAL.ecsv" + infile = os.path.join(resources.files('zdm'), 'data', 'optical', datafile) + table = Table.read(infile, format='ascii.ecsv') + return table + def get_pz_prior(grid, DM): """ Returns posterior redshift distributiuon for a given grid and DM diff --git a/zdm/optical_numerics.py b/zdm/optical_numerics.py index 713e367e..ac8d1e61 100644 --- a/zdm/optical_numerics.py +++ b/zdm/optical_numerics.py @@ -29,6 +29,7 @@ def function(x,args): ss = args[1] gs=args[2] model=args[3] + POxcut=args[4] # either None, or a cut such as 0.9 # initialises model to the priors # generates one per grid, due to possible different zvals @@ -41,7 +42,8 @@ def function(x,args): # we re-normalise the sum of PUs by NFRB # prevents infinite plots being created - stat = calculate_goodness_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,sumPUobs,sumPUprior,plotfile=None) + stat = calculate_goodness_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors, + sumPUobs,sumPUprior,plotfile=None,POxcut=POxcut) return stat @@ -179,7 +181,8 @@ def calc_path_priors(frblist,ss,gs,wrappers,verbose=True,usemodel=True): return nfitted,AppMags,allMagPriors,allObsMags,allPOx,allPU,allPUx,sumPU,sumPUx -def calculate_goodness_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUobs,PUprior,plotfile=None): +def calculate_goodness_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUobs, + PUprior,plotfile=None,POxcut=None): """ Calculates a ks-like statistics to be proxy for goodness-of-fit We must set each AppMagPriors to 1.-PUprior at the limiting magnitude for each observation, @@ -196,7 +199,8 @@ def calculate_goodness_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors ObsPosteriors: list of posterior values corresponding to ObsMags PUobs: posterior on unseen probability PUprior: prior on PU - Plotfile: set to name of output file for comparison plot + plotfile: set to name of output file for comparison plot + POxcut: if not None, cut data to fixed POx. Used to simulate current techniques Returns: k-like statistic of biggest obs/prior difference @@ -205,14 +209,26 @@ def calculate_goodness_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors # we calculate a probability using a cumulative distribution prior_dist = np.cumsum(AppMagPriors) - # the above is normalised to NFRB. We now divide it by this - # might want to be careful here, and preserve this normalisation - prior_dist /= NFRB #((NFRB-PUprior)/NFRB) / prior_dist[-1] + if POxcut is not None: + # cuts data to "good" FRBs only + OK = np.where(ObsPosteriors > POxcut)[0] + Ndata = len(OK) + ObsMags = ObsMags[OK] + ObsPosteriors = np.full([Ndata],1.) # effectively sets these to unity + # makes a cdf in units of AppMags, with observations ObsMags weighted by ObsPosteriors obs_dist = make_cdf(AppMags,ObsMags,ObsPosteriors,norm=False) - obs_dist /= NFRB + if POxcut is not None: + # current techniques just assume we have the full distribution + obs_dist /= obs_dist[-1] + prior_dist /= prior_dist[-1] + else: + # the above is normalised to NFRB. We now divide it by this + # might want to be careful here, and preserve this normalisation + obs_dist /= NFRB + prior_dist /= NFRB #((NFRB-PUprior)/NFRB) / prior_dist[-1] # we calculate something like the k-statistic. Includes NFRB normalisation diff = obs_dist - prior_dist diff --git a/zdm/scripts/Path/optimise_host_priors.py b/zdm/scripts/Path/optimise_host_priors.py index 7a1e744d..91cf55de 100644 --- a/zdm/scripts/Path/optimise_host_priors.py +++ b/zdm/scripts/Path/optimise_host_priors.py @@ -59,7 +59,9 @@ def main(): ss,gs = loading.surveys_and_grids(survey_names=names) modelname = "loudas" - opdir = modelname+"_output/" + opdir = modelname+"_0.9_output/" + POxcut = None # set to e.g. 0.9 to reject FRBs with lower posteriors when doing model comparisons + if not os.path.exists(opdir): os.mkdir(opdir) @@ -78,7 +80,7 @@ def main(): # initialise aguments to minimisation function - args=[frblist,ss,gs,model] + args=[frblist,ss,gs,model,POxcut] Nparams = len(x0) bounds = [(0,1)]*Nparams @@ -155,8 +157,11 @@ def main(): #outfile = None model.init_args(sfr) wrappers = on.make_wrappers(model,gs) - NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUprior,PUobs,sumPUprior,sumPUobs = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) - stat = on.calculate_goodness_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,sumPUobs,sumPUprior,plotfile=outfile) + NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUprior,\ + PUobs,sumPUprior,sumPUobs = on.calc_path_priors(frblist, + ss,gs,wrappers,verbose=False) + stat = on.calculate_goodness_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,sumPUobs, + sumPUprior,plotfile=outfile,POxcut=POxcut) stats[istat] = stat outfile = opdir+"scan_sfr.png" plt.figure() diff --git a/zdm/scripts/Path/plot_host_models.py b/zdm/scripts/Path/plot_host_models.py index 712ee364..bce29496 100644 --- a/zdm/scripts/Path/plot_host_models.py +++ b/zdm/scripts/Path/plot_host_models.py @@ -87,8 +87,28 @@ def calc_path_priors(): plt.legend() plt.tight_layout() plt.savefig(opdir+"loudas_model_mags.png") - plt.close() + plt.close() + + + ###### Gives examples for fsfr at weird values ##### + plt.figure() + plt.xlabel("$m_r$") + plt.ylabel("p(m_r | z=0.5)$") + + fsfrs = np.linspace(-2,3,6) # extrapolates to weird values + z=0.5 + mrbins = np.linspace(0,40,401) + rbc = (mrbins[1:] + mrbins[:-1])/2. + for i,fsfr in enumerate(fsfrs): + model2.init_args(fsfr) + pmr = model2.get_pmr_gz(mrbins,z) + plt.plot(rbc,pmr,label="$f_{\\rm sfr} = $"+str(fsfr)[:5]) + plt.xlim(15,30) + plt.legend() + plt.tight_layout() + plt.savefig(opdir+"loudas_fsfr_interpolation.png") + plt.close() # set up basic histogram of p(mr) distribution mrbins = np.linspace(0,30,301) diff --git a/zdm/states.py b/zdm/states.py index afb04ebc..d8c34345 100644 --- a/zdm/states.py +++ b/zdm/states.py @@ -84,7 +84,6 @@ def load_state(case="HoffmannHalo25",scat=None,rep=None): # update with relevant values state.update_param_dict(vparams) - return state def set_reps(vparams,rep): @@ -377,7 +376,7 @@ def set_updated_scat(vparams): if not 'scat' in vparams: - vparams['width'] = {} + vparams['scat'] = {} vparams['scat'] = {} vparams['scat']['Slogmean'] = -1.3 vparams['scat']['Slogsigma'] = 0.2 From 62ef1db82ea0c272021c9e2511f10734b6628553 Mon Sep 17 00:00:00 2001 From: Clancy James Date: Thu, 8 Jan 2026 11:10:56 +0800 Subject: [PATCH 10/35] Moved Bryce's fake survey creation script to appropriate lsst directory --- .../lsst/Photometric}/Smeared.ecsv | 0 .../lsst/Photometric}/Smeared_and_zFrac.ecsv | 0 .../lsst/Photometric}/Spectroscopic.ecsv | 0 .../lsst/Photometric}/create_fake_survey.py | 85 +++++++++--------- .../lsst/Photometric}/zFrac.ecsv | 0 zdm/data/Optical/fz_24.7.npy | Bin 4928 -> 0 bytes .../PlotIndividualExperiments/plot_SKA.py | 4 +- 7 files changed, 47 insertions(+), 42 deletions(-) rename {zdm/data/Surveys => papers/lsst/Photometric}/Smeared.ecsv (100%) rename {zdm/data/Surveys => papers/lsst/Photometric}/Smeared_and_zFrac.ecsv (100%) rename {zdm/data/Surveys => papers/lsst/Photometric}/Spectroscopic.ecsv (100%) rename {zdm/scripts => papers/lsst/Photometric}/create_fake_survey.py (70%) rename {zdm/data/Surveys => papers/lsst/Photometric}/zFrac.ecsv (100%) delete mode 100644 zdm/data/Optical/fz_24.7.npy diff --git a/zdm/data/Surveys/Smeared.ecsv b/papers/lsst/Photometric/Smeared.ecsv similarity index 100% rename from zdm/data/Surveys/Smeared.ecsv rename to papers/lsst/Photometric/Smeared.ecsv diff --git a/zdm/data/Surveys/Smeared_and_zFrac.ecsv b/papers/lsst/Photometric/Smeared_and_zFrac.ecsv similarity index 100% rename from zdm/data/Surveys/Smeared_and_zFrac.ecsv rename to papers/lsst/Photometric/Smeared_and_zFrac.ecsv diff --git a/zdm/data/Surveys/Spectroscopic.ecsv b/papers/lsst/Photometric/Spectroscopic.ecsv similarity index 100% rename from zdm/data/Surveys/Spectroscopic.ecsv rename to papers/lsst/Photometric/Spectroscopic.ecsv diff --git a/zdm/scripts/create_fake_survey.py b/papers/lsst/Photometric/create_fake_survey.py similarity index 70% rename from zdm/scripts/create_fake_survey.py rename to papers/lsst/Photometric/create_fake_survey.py index 8cb09ccf..9c48f52a 100644 --- a/zdm/scripts/create_fake_survey.py +++ b/papers/lsst/Photometric/create_fake_survey.py @@ -11,54 +11,57 @@ from zdm import loading from zdm import io from zdm import optical as opt +from zdm import states from matplotlib import pyplot as plt from numpy import random import numpy as np from zdm import survey from matplotlib import pyplot as plt -from pkg_resources import resource_filename +import importlib.resources as resources def create_fake_survey(smearing=False): - path = os.path.join(resource_filename('zdm', 'data'), 'Surveys')+"/" - file="Spectroscopic.ecsv" + sdir = str(resources.files('zdm').joinpath('data/Surveys')) + opdir="./" # directory to place fake surveys in. Here! IntroStr="""# %ECSV 1.0 -# --- -# datatype: -# - {name: TNS, datatype: string} -# - {name: DM, datatype: float64} -# - {name: RA, datatype: string} -# - {name: DEC, datatype: string} -# - {name: Z, datatype: float64} -# - {name: SNR, datatype: float64} -# - {name: WIDTH, datatype: float64} -# - {name: Gl, unit: deg, datatype: float64} -# - {name: Gb, unit: deg, datatype: float64} -# - {name: DMG, datatype: float64} -# - {name: FBAR, datatype: float64} -# - {name: BW, datatype: float64} -# meta: !!omap -# - {survey_data: '{"observing": {"NORM_FRB": 17,"TOBS": 64.68,"MAX_IW": 8, "MAXWMETH": 2}, -# "telescope": {"BEAM": "CRACO_900", "DMMASK": "craco_900_mask.npy", -# "DIAM": 12.0, "NBEAMS": 1, "NBINS": 5, "FBAR": 906, -# "TRES": 13.8, "FRES": 1.0, "THRESH": 1.01}}'}\n""" - - param_dict={'sfr_n': 0.21, 'alpha': 0.11, 'lmean': 2.18, 'lsigma': 0.42, 'lEmax': 41.37, - 'lEmin': 39.47, 'gamma': -1.04, 'H0': 70.23, 'halo_method': 0, 'sigmaDMG': 0.0, 'sigmaHalo': 0.0,'lC': -7.61} - state=parameters.State() - state.set_astropy_cosmo(Planck18) - state.update_params(param_dict) + # --- + # datatype: + # - {name: TNS, datatype: string} + # - {name: DM, datatype: float64} + # - {name: RA, datatype: string} + # - {name: DEC, datatype: string} + # - {name: Z, datatype: float64} + # - {name: SNR, datatype: float64} + # - {name: WIDTH, datatype: float64} + # - {name: Gl, unit: deg, datatype: float64} + # - {name: Gb, unit: deg, datatype: float64} + # - {name: DMG, datatype: float64} + # - {name: FBAR, datatype: float64} + # - {name: BW, datatype: float64} + # meta: !!omap + # - {survey_data: '{"observing": {"NORM_FRB": 17,"TOBS": 64.68,"MAX_IW": 8, "MAXWMETH": 2}, + # "telescope": {"BEAM": "CRACO_900", "DMMASK": "craco_900_mask.npy", + # "DIAM": 12.0, "NBEAMS": 1, "NBINS": 5, "FBAR": 906, + # "TRES": 13.8, "FRES": 1.0, "THRESH": 1.01}}'}\n""" + + #param_dict={'sfr_n': 0.21, 'alpha': 0.11, 'lmean': 2.18, 'lsigma': 0.42, 'lEmax': 41.37, + # 'lEmin': 39.47, 'gamma': -1.04, 'H0': 70.23, 'halo_method': 0, 'sigmaDMG': 0.0, 'sigmaHalo': 0.0,'lC': -7.61} + + # use default state + state=states.load_state(case="HoffmannHalo25",scat=None,rep=None) + #state.set_astropy_cosmo(Planck18) + #state.update_params(param_dict) name=['CRAFT_CRACO_900'] - sdir = os.path.join(resource_filename('zdm', 'data'), 'Surveys') + ss,gs=loading.surveys_and_grids(survey_names=name,repeaters=False,init_state=state,sdir=sdir) gs=gs[0] #gs.state.photo.smearing=smearing gs.calc_rates() samples=gs.GenMCSample(100) zvals=np.zeros(len(samples)) - fp=open(path+file,"w+") + fp=open(opdir+"Spectroscopic.ecsv","w+") fp.write(IntroStr) fp.write("TNS DM RA DEC Z SNR WIDTH Gl Gb DMG FBAR BW\n") for i in range(len(samples)): @@ -76,14 +79,16 @@ def create_fake_survey(smearing=False): fp.write('{0:8}'.format("288")) fp.write("\n") fp.close() - + + # We now smear the redshift values by the z-error + if smearing is True: sigmas=np.array([0.035]) for sigma in sigmas: for i in range(len(samples)): zvals[i]=samples[i][0] - file="Smeared.ecsv" - fp=open(path+file,"w+") + + fp=open(opdir+"Smeared.ecsv","w+") fp.write(IntroStr) fp.write("TNS DM RA DEC Z SNR WIDTH Gl Gb DMG FBAR BW\n") smear_error=random.normal(loc=0,scale=sigma,size=100) @@ -103,13 +108,13 @@ def create_fake_survey(smearing=False): fp.write('{0:8}'.format("288")) fp.write("\n") fp.close() - - frac_path=path+"/../Optical/" - fz=np.load(frac_path+"lsst_24.7_fz.npy")[0:500] - zs=np.load(frac_path+"lsst_24.7_z.npy")[0:500] - file="zFrac.ecsv" - fp=open(path+file,"w+") - fp1=open(path+"Smeared_and_zFrac.ecsv","w+") + + frac_path = str(resources.files('zdm').joinpath('../papers/lsst/Data')) + fz=np.load(frac_path+"/fz_24.7.npy")[0:500] + zs=np.load(frac_path+"/zvals.npy")[0:500] + + fp=open(opdir+"zFrac.ecsv","w+") + fp1=open(opdir+"Smeared_and_zFrac.ecsv","w+") fp.write(IntroStr) fp1.write(IntroStr) fp.write("TNS DM RA DEC Z SNR WIDTH Gl Gb DMG FBAR BW\n") diff --git a/zdm/data/Surveys/zFrac.ecsv b/papers/lsst/Photometric/zFrac.ecsv similarity index 100% rename from zdm/data/Surveys/zFrac.ecsv rename to papers/lsst/Photometric/zFrac.ecsv diff --git a/zdm/data/Optical/fz_24.7.npy b/zdm/data/Optical/fz_24.7.npy deleted file mode 100644 index ab4c1bbad73719637f120da214449ae844e976b0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4928 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 diff --git a/zdm/scripts/PlotIndividualExperiments/plot_SKA.py b/zdm/scripts/PlotIndividualExperiments/plot_SKA.py index c23c5902..11a23e01 100644 --- a/zdm/scripts/PlotIndividualExperiments/plot_SKA.py +++ b/zdm/scripts/PlotIndividualExperiments/plot_SKA.py @@ -20,7 +20,7 @@ import numpy as np from zdm import survey from matplotlib import pyplot as plt -from pkg_resources import resource_filename +import importlib.resources as resources def main(): @@ -33,7 +33,7 @@ def main(): os.mkdir(opdir) # Initialise surveys and grids - sdir = os.path.join(resource_filename('zdm', 'data'), 'Surveys') + sdir = resources.files('zdm').joinpath('data/Surveys') names=['SKA_mid'] state = parameters.State() # approximate best-fit values from recent analysis From b9278c4265ba04a5791eaf8c379bb41a7d801dfd Mon Sep 17 00:00:00 2001 From: Clancy James Date: Thu, 8 Jan 2026 17:19:26 +0800 Subject: [PATCH 11/35] finalised Bryces photometric analysis --- papers/lsst/Photometric/Smeared.ecsv | 202 +++++++-------- .../lsst/Photometric/Smeared_and_zFrac.ecsv | 161 ++++++------ papers/lsst/Photometric/Spectroscopic.ecsv | 200 +++++++-------- papers/lsst/Photometric/create_fake_survey.py | 54 ++-- papers/lsst/Photometric/plot_2dgrids.py | 84 +++++++ papers/lsst/Photometric/plot_H0_slice.py | 103 ++++++++ papers/lsst/Photometric/run_H0_slice.py | 100 ++++++++ .../lsst/Photometric}/run_slice.py | 0 papers/lsst/Photometric/zFrac.ecsv | 161 ++++++------ papers/lsst/sim_pz.py | 22 +- zdm/grid.py | 89 ++++--- zdm/iteration.py | 2 +- zdm/scripts/H0_scan/run_H0_slice.py | 238 ------------------ zdm/survey.py | 1 + zdm/survey_data.py | 5 +- 15 files changed, 746 insertions(+), 676 deletions(-) create mode 100644 papers/lsst/Photometric/plot_2dgrids.py create mode 100644 papers/lsst/Photometric/plot_H0_slice.py create mode 100644 papers/lsst/Photometric/run_H0_slice.py rename {zdm/scripts/H0_scan => papers/lsst/Photometric}/run_slice.py (100%) delete mode 100644 zdm/scripts/H0_scan/run_H0_slice.py diff --git a/papers/lsst/Photometric/Smeared.ecsv b/papers/lsst/Photometric/Smeared.ecsv index 13988112..59ddb7d7 100644 --- a/papers/lsst/Photometric/Smeared.ecsv +++ b/papers/lsst/Photometric/Smeared.ecsv @@ -14,108 +14,108 @@ # - {name: FBAR, datatype: float64} # - {name: BW, datatype: float64} # meta: !!omap -# - {survey_data: '{"observing": {"NORM_FRB": 17,"TOBS": 64.68,"MAX_IW": 8, "MAXWMETH": 2}, +# - {survey_data: '{"observing": {"NORM_FRB": 17,"TOBS": 64.68,"MAX_IW": 8, "MAXWMETH": 2, "Z_PHOTO": 0.03}, # "telescope": {"BEAM": "CRACO_900", "DMMASK": "craco_900_mask.npy", # "DIAM": 12.0, "NBEAMS": 1, "NBINS": 5, "FBAR": 906, # "TRES": 13.8, "FRES": 1.0, "THRESH": 1.01}}'} TNS DM RA DEC Z SNR WIDTH Gl Gb DMG FBAR BW -0 2334.5710018337304 00:00:00 00:00:00 3.015427828987245 42.9076170576045 -1.0 -1.0 -1.0 35.0 888 288 -1 1293.8332446347993 00:00:00 00:00:00 0.8026726589177778 12.346871755650161 -1.0 -1.0 -1.0 35.0 888 288 -2 1130.9791692249696 00:00:00 00:00:00 0.6131175556212196 16.45799179528472 -1.0 -1.0 -1.0 35.0 888 288 -3 948.9962810274221 00:00:00 00:00:00 1.0729273467300278 12.899405096865053 -1.0 -1.0 -1.0 35.0 888 288 -4 3302.7414319309637 00:00:00 00:00:00 3.4526582385587026 10.896087814782916 -1.0 -1.0 -1.0 35.0 888 288 -5 789.2339950025221 00:00:00 00:00:00 0.22440979482747994 15.974081920078653 -1.0 -1.0 -1.0 35.0 888 288 -6 322.2960237321481 00:00:00 00:00:00 0.30011338886437156 41.61528388827413 -1.0 -1.0 -1.0 35.0 888 288 -7 291.26523564494755 00:00:00 00:00:00 0.3716353845205117 35.39286498287345 -1.0 -1.0 -1.0 35.0 888 288 -8 265.6172817387779 00:00:00 00:00:00 0.07031358571327158 17.005499594805812 -1.0 -1.0 -1.0 35.0 888 288 -9 837.3735197561044 00:00:00 00:00:00 0.6925267387932372 22.368861955427324 -1.0 -1.0 -1.0 35.0 888 288 -10 430.42288234797786 00:00:00 00:00:00 0.5453395634766126 31.14684691070062 -1.0 -1.0 -1.0 35.0 888 288 -11 338.8720812210228 00:00:00 00:00:00 0.4757401036066104 11.514360186435827 -1.0 -1.0 -1.0 35.0 888 288 -12 459.8929794701774 00:00:00 00:00:00 0.6111553250126587 28.970291709672008 -1.0 -1.0 -1.0 35.0 888 288 -13 514.2392669873378 00:00:00 00:00:00 0.48320447158887225 26.23234752429593 -1.0 -1.0 -1.0 35.0 888 288 -14 444.82316308571853 00:00:00 00:00:00 0.16778987448479044 10.224780135231132 -1.0 -1.0 -1.0 35.0 888 288 -15 423.4287617724789 00:00:00 00:00:00 0.12667007201296906 16.93166467819824 -1.0 -1.0 -1.0 35.0 888 288 -16 250.18714908014232 00:00:00 00:00:00 0.1591634140141581 16.825688142584564 -1.0 -1.0 -1.0 35.0 888 288 -17 230.04604721703424 00:00:00 00:00:00 0.20900964741015637 30.06214700360865 -1.0 -1.0 -1.0 35.0 888 288 -18 620.1606829061071 00:00:00 00:00:00 0.3354237408317748 48.42640123698046 -1.0 -1.0 -1.0 35.0 888 288 -19 548.9677620404942 00:00:00 00:00:00 0.16933329054962168 14.25071741051486 -1.0 -1.0 -1.0 35.0 888 288 -20 208.45270441661265 00:00:00 00:00:00 0.2175794326593667 11.795177754311347 -1.0 -1.0 -1.0 35.0 888 288 -21 1456.3273969505499 00:00:00 00:00:00 1.128451983662971 15.265200782969 -1.0 -1.0 -1.0 35.0 888 288 -22 454.31237113237376 00:00:00 00:00:00 0.3285218412623604 14.83980914864711 -1.0 -1.0 -1.0 35.0 888 288 -23 188.43753414368308 00:00:00 00:00:00 0.1006293027745211 26.190444971253193 -1.0 -1.0 -1.0 35.0 888 288 -24 432.25074329840373 00:00:00 00:00:00 0.43820309825233555 46.827196897045056 -1.0 -1.0 -1.0 35.0 888 288 -25 579.5866100250605 00:00:00 00:00:00 0.8228428464865679 15.689862449648482 -1.0 -1.0 -1.0 35.0 888 288 -26 5036.7746606005 00:00:00 00:00:00 0.4655133704578701 11.894270840857446 -1.0 -1.0 -1.0 35.0 888 288 -27 143.49252810767797 00:00:00 00:00:00 0.04138013323734881 484.69318911542297 -1.0 -1.0 -1.0 35.0 888 288 -28 896.2325667562126 00:00:00 00:00:00 0.8874222183296216 18.995660966151245 -1.0 -1.0 -1.0 35.0 888 288 -29 695.548826635149 00:00:00 00:00:00 0.6072055708319989 12.325923688964098 -1.0 -1.0 -1.0 35.0 888 288 -30 518.6973214909851 00:00:00 00:00:00 0.1858292681019909 40.622240538994284 -1.0 -1.0 -1.0 35.0 888 288 -31 820.101280852925 00:00:00 00:00:00 1.1907785396605095 11.906421530120177 -1.0 -1.0 -1.0 35.0 888 288 -32 805.6084061258246 00:00:00 00:00:00 0.2539606628978965 11.353905426206259 -1.0 -1.0 -1.0 35.0 888 288 -33 419.29611869030475 00:00:00 00:00:00 0.15958310375490395 16.60160673523299 -1.0 -1.0 -1.0 35.0 888 288 -34 155.6073504242375 00:00:00 00:00:00 0.14762739361862726 66.13285676320632 -1.0 -1.0 -1.0 35.0 888 288 -35 1147.6509354631635 00:00:00 00:00:00 1.0196449201386366 10.989377138257774 -1.0 -1.0 -1.0 35.0 888 288 -36 434.9054218021994 00:00:00 00:00:00 0.36587030490427747 238.5088838108578 -1.0 -1.0 -1.0 35.0 888 288 -37 310.16626884466916 00:00:00 00:00:00 0.18569892608759028 14.360447206185484 -1.0 -1.0 -1.0 35.0 888 288 -38 257.9624832084242 00:00:00 00:00:00 0.18154696874743803 21.270338041223077 -1.0 -1.0 -1.0 35.0 888 288 -39 323.316432591907 00:00:00 00:00:00 0.3429240963699535 40.66596088103641 -1.0 -1.0 -1.0 35.0 888 288 -40 387.26342412555556 00:00:00 00:00:00 0.5271999423649261 61.00023532034031 -1.0 -1.0 -1.0 35.0 888 288 -41 275.28452072976125 00:00:00 00:00:00 0.09048525492849627 48.83811893549135 -1.0 -1.0 -1.0 35.0 888 288 -42 1067.4327278110532 00:00:00 00:00:00 0.08708632585333373 20.27598540262997 -1.0 -1.0 -1.0 35.0 888 288 -43 730.1731714209269 00:00:00 00:00:00 0.44597961391510366 11.312977703982753 -1.0 -1.0 -1.0 35.0 888 288 -44 879.3689116020875 00:00:00 00:00:00 0.6714204031929973 20.44890692681992 -1.0 -1.0 -1.0 35.0 888 288 -45 140.53262627190654 00:00:00 00:00:00 0.09784407401506416 12.864108327165525 -1.0 -1.0 -1.0 35.0 888 288 -46 138.60386510840408 00:00:00 00:00:00 0.11280585785856893 67.09929347610452 -1.0 -1.0 -1.0 35.0 888 288 -47 457.39367798062347 00:00:00 00:00:00 0.3400258623394666 126.10472883527777 -1.0 -1.0 -1.0 35.0 888 288 -48 720.9874661585104 00:00:00 00:00:00 0.649765717228654 14.19983264302042 -1.0 -1.0 -1.0 35.0 888 288 -49 272.4500208102785 00:00:00 00:00:00 0.13769590971746024 23.153353203332813 -1.0 -1.0 -1.0 35.0 888 288 -50 229.67723852719107 00:00:00 00:00:00 0.12006320865875231 15.939674250127918 -1.0 -1.0 -1.0 35.0 888 288 -51 331.58825171496477 00:00:00 00:00:00 0.25698668054291335 35.76985881730512 -1.0 -1.0 -1.0 35.0 888 288 -52 775.9942188826124 00:00:00 00:00:00 0.8964095584886425 20.787372046573893 -1.0 -1.0 -1.0 35.0 888 288 -53 491.54267256019284 00:00:00 00:00:00 0.4270363735922269 15.873128885187782 -1.0 -1.0 -1.0 35.0 888 288 -54 529.128624543456 00:00:00 00:00:00 0.13467652357181403 30.946910009847507 -1.0 -1.0 -1.0 35.0 888 288 -55 399.1055744918342 00:00:00 00:00:00 0.2859800906559676 10.88583325549367 -1.0 -1.0 -1.0 35.0 888 288 -56 431.0128668027263 00:00:00 00:00:00 0.3378338742731474 10.468990242397023 -1.0 -1.0 -1.0 35.0 888 288 -57 190.52132889218325 00:00:00 00:00:00 0.17799965496545547 16.66318259472948 -1.0 -1.0 -1.0 35.0 888 288 -58 113.15491238877307 00:00:00 00:00:00 0.039021369275481325 52.02458333198281 -1.0 -1.0 -1.0 35.0 888 288 -59 559.3752828715894 00:00:00 00:00:00 0.6988831678653942 21.37087749061021 -1.0 -1.0 -1.0 35.0 888 288 -60 1013.2533721372135 00:00:00 00:00:00 0.7551348790699648 14.372771247543605 -1.0 -1.0 -1.0 35.0 888 288 -61 532.6898941820887 00:00:00 00:00:00 0.18621399996785953 29.232208234528766 -1.0 -1.0 -1.0 35.0 888 288 -62 1042.8613965606653 00:00:00 00:00:00 1.1905415071138556 12.063938525273805 -1.0 -1.0 -1.0 35.0 888 288 -63 911.7804794768108 00:00:00 00:00:00 1.2795936353491515 14.290569605992246 -1.0 -1.0 -1.0 35.0 888 288 -64 622.5786144978405 00:00:00 00:00:00 0.2325455138564069 86.80356920697237 -1.0 -1.0 -1.0 35.0 888 288 -65 691.7803873564447 00:00:00 00:00:00 0.5529703609657931 12.899063417967223 -1.0 -1.0 -1.0 35.0 888 288 -66 425.79471677447805 00:00:00 00:00:00 0.5556620352238183 18.002963736786747 -1.0 -1.0 -1.0 35.0 888 288 -67 897.0848483985724 00:00:00 00:00:00 0.8034890084589824 14.75432572849072 -1.0 -1.0 -1.0 35.0 888 288 -68 414.8587456661204 00:00:00 00:00:00 0.310070672785909 10.944184015097571 -1.0 -1.0 -1.0 35.0 888 288 -69 663.4430327655946 00:00:00 00:00:00 0.731575569293054 30.236972085102074 -1.0 -1.0 -1.0 35.0 888 288 -70 148.69756109291183 00:00:00 00:00:00 0.10085726227990287 86.90035422142955 -1.0 -1.0 -1.0 35.0 888 288 -71 922.4396243398675 00:00:00 00:00:00 1.175933579767771 19.921968002308613 -1.0 -1.0 -1.0 35.0 888 288 -72 152.9383064815016 00:00:00 00:00:00 0.06045578514048208 40.184792633110376 -1.0 -1.0 -1.0 35.0 888 288 -73 1294.9170003315799 00:00:00 00:00:00 0.7658583676399973 10.960537313759053 -1.0 -1.0 -1.0 35.0 888 288 -74 197.05046287007687 00:00:00 00:00:00 0.3250564943880785 11.866829353300446 -1.0 -1.0 -1.0 35.0 888 288 -75 1152.0351297397929 00:00:00 00:00:00 1.2772288625892987 22.166897711183683 -1.0 -1.0 -1.0 35.0 888 288 -76 574.7793210141199 00:00:00 00:00:00 0.15837600883038636 19.45858882312784 -1.0 -1.0 -1.0 35.0 888 288 -77 1001.7697964755914 00:00:00 00:00:00 1.0036542633334602 12.261260069068786 -1.0 -1.0 -1.0 35.0 888 288 -78 155.39833051180779 00:00:00 00:00:00 0.19347898107465472 15.413819616047661 -1.0 -1.0 -1.0 35.0 888 288 -79 721.4914716203449 00:00:00 00:00:00 0.30295383251025304 15.099468387889068 -1.0 -1.0 -1.0 35.0 888 288 -80 544.2632638190482 00:00:00 00:00:00 0.25848937308392594 16.937293035041506 -1.0 -1.0 -1.0 35.0 888 288 -81 413.7706136774752 00:00:00 00:00:00 0.3861876593421423 45.53821522890108 -1.0 -1.0 -1.0 35.0 888 288 -82 512.2968332797499 00:00:00 00:00:00 0.19186009644515764 65.41049706254961 -1.0 -1.0 -1.0 35.0 888 288 -83 604.6614773741395 00:00:00 00:00:00 0.5922496204950911 87.47806150668559 -1.0 -1.0 -1.0 35.0 888 288 -84 1518.9511344791895 00:00:00 00:00:00 0.5221800070467364 21.702413338435306 -1.0 -1.0 -1.0 35.0 888 288 -85 643.7854589158669 00:00:00 00:00:00 0.20310544997982768 10.123746839702076 -1.0 -1.0 -1.0 35.0 888 288 -86 126.27385897827892 00:00:00 00:00:00 0.14890248371153225 14.943311830534453 -1.0 -1.0 -1.0 35.0 888 288 -87 1301.6620748304958 00:00:00 00:00:00 1.5784862256072505 12.191300403208118 -1.0 -1.0 -1.0 35.0 888 288 -88 108.5822318161769 00:00:00 00:00:00 0.004906613899604562 98.37197874300217 -1.0 -1.0 -1.0 35.0 888 288 -89 454.4376843789279 00:00:00 00:00:00 0.1806707830480725 11.962983358961118 -1.0 -1.0 -1.0 35.0 888 288 -90 431.94315540593266 00:00:00 00:00:00 0.44017691427265276 46.19921231942614 -1.0 -1.0 -1.0 35.0 888 288 -91 563.7835858233838 00:00:00 00:00:00 0.5942396002726046 12.142815139562412 -1.0 -1.0 -1.0 35.0 888 288 -92 337.3005917385242 00:00:00 00:00:00 0.4810711931332526 14.345395179118563 -1.0 -1.0 -1.0 35.0 888 288 -93 147.4661489599992 00:00:00 00:00:00 -0.014431551553354828 504.1131760754106 -1.0 -1.0 -1.0 35.0 888 288 -94 358.8683383314342 00:00:00 00:00:00 0.3062464185841798 18.326073205181974 -1.0 -1.0 -1.0 35.0 888 288 -95 326.5453849076442 00:00:00 00:00:00 0.4124803983720404 12.654073325103571 -1.0 -1.0 -1.0 35.0 888 288 -96 427.76775974005335 00:00:00 00:00:00 0.2810773811596752 25.75055977593358 -1.0 -1.0 -1.0 35.0 888 288 -97 1537.250617119628 00:00:00 00:00:00 0.8858851936214186 16.729141697437374 -1.0 -1.0 -1.0 35.0 888 288 -98 463.1233303504703 00:00:00 00:00:00 0.5140920482601581 11.12631974466657 -1.0 -1.0 -1.0 35.0 888 288 -99 323.04706650622984 00:00:00 00:00:00 0.23990636996638962 44.49101643122364 -1.0 -1.0 -1.0 35.0 888 288 +0 934.0882936484263 00:00:00 00:00:00 0.785778162562317 29.141753808504433 -1.0 -1.0 -1.0 35.0 888 288 +1 616.5738602258931 00:00:00 00:00:00 0.8349135358303849 10.160405042320036 -1.0 -1.0 -1.0 35.0 888 288 +2 946.6052448796315 00:00:00 00:00:00 0.6474993506647525 10.652772181822607 -1.0 -1.0 -1.0 35.0 888 288 +3 586.5727013781997 00:00:00 00:00:00 0.12495187173178796 12.415341138097366 -1.0 -1.0 -1.0 35.0 888 288 +4 2153.3992719321486 00:00:00 00:00:00 0.8036468331892808 12.24289806494758 -1.0 -1.0 -1.0 35.0 888 288 +5 1343.7265012800563 00:00:00 00:00:00 1.727489385549202 11.09240844582027 -1.0 -1.0 -1.0 35.0 888 288 +6 529.8895060996854 00:00:00 00:00:00 0.603211700191891 13.133173041616157 -1.0 -1.0 -1.0 35.0 888 288 +7 1022.7800218616562 00:00:00 00:00:00 1.2888620684968957 10.300851732186109 -1.0 -1.0 -1.0 35.0 888 288 +8 412.87460299855934 00:00:00 00:00:00 0.21865062413724176 12.846680716802911 -1.0 -1.0 -1.0 35.0 888 288 +9 701.6217045820609 00:00:00 00:00:00 0.7299571477022802 10.341966201772214 -1.0 -1.0 -1.0 35.0 888 288 +10 205.82201397136845 00:00:00 00:00:00 0.06074866029287343 20.941567140489564 -1.0 -1.0 -1.0 35.0 888 288 +11 1120.7016388974587 00:00:00 00:00:00 1.1724572933789503 11.41994898605589 -1.0 -1.0 -1.0 35.0 888 288 +12 275.5937204880448 00:00:00 00:00:00 0.09245951767772358 91.38695892704916 -1.0 -1.0 -1.0 35.0 888 288 +13 436.349204138444 00:00:00 00:00:00 0.44564144183478205 15.275450861578175 -1.0 -1.0 -1.0 35.0 888 288 +14 295.6406494853049 00:00:00 00:00:00 0.1442212477744424 11.219409175951904 -1.0 -1.0 -1.0 35.0 888 288 +15 291.6471440894715 00:00:00 00:00:00 0.2990568845451625 11.18814447805394 -1.0 -1.0 -1.0 35.0 888 288 +16 1618.8558318442904 00:00:00 00:00:00 0.9448819250948836 24.749670081652773 -1.0 -1.0 -1.0 35.0 888 288 +17 983.9563468218174 00:00:00 00:00:00 0.5110387741801308 12.591584618769005 -1.0 -1.0 -1.0 35.0 888 288 +18 676.3365646959189 00:00:00 00:00:00 0.8336236470816221 10.476494180170306 -1.0 -1.0 -1.0 35.0 888 288 +19 645.2399497353754 00:00:00 00:00:00 0.49888042615577133 10.286082085018421 -1.0 -1.0 -1.0 35.0 888 288 +20 539.5953081699104 00:00:00 00:00:00 0.6182169860530876 35.45318302068964 -1.0 -1.0 -1.0 35.0 888 288 +21 443.6632526190763 00:00:00 00:00:00 0.596702703542215 14.319328661152863 -1.0 -1.0 -1.0 35.0 888 288 +22 81.45567951589115 00:00:00 00:00:00 0.06896887062626902 15.566080612070119 -1.0 -1.0 -1.0 35.0 888 288 +23 374.91893100970947 00:00:00 00:00:00 0.08654025153807617 29.766613679561118 -1.0 -1.0 -1.0 35.0 888 288 +24 1001.3592175483924 00:00:00 00:00:00 1.1219928711003506 12.965473066837115 -1.0 -1.0 -1.0 35.0 888 288 +25 711.626862396593 00:00:00 00:00:00 0.14070511599819086 13.229440860953233 -1.0 -1.0 -1.0 35.0 888 288 +26 356.4667997984884 00:00:00 00:00:00 0.44644296786158993 20.3929051321851 -1.0 -1.0 -1.0 35.0 888 288 +27 405.6849410353128 00:00:00 00:00:00 0.43892086227384686 12.020783061083032 -1.0 -1.0 -1.0 35.0 888 288 +28 438.56039965043755 00:00:00 00:00:00 0.41899323244205144 14.962222736067005 -1.0 -1.0 -1.0 35.0 888 288 +29 1032.0031340800927 00:00:00 00:00:00 1.0111782328413856 12.249965125983609 -1.0 -1.0 -1.0 35.0 888 288 +30 217.45075943955328 00:00:00 00:00:00 0.18461768474454746 13.329278119983234 -1.0 -1.0 -1.0 35.0 888 288 +31 337.87773894962976 00:00:00 00:00:00 0.3539648344531408 15.842464362846469 -1.0 -1.0 -1.0 35.0 888 288 +32 747.9605061576287 00:00:00 00:00:00 0.40398282515571504 11.35266012864496 -1.0 -1.0 -1.0 35.0 888 288 +33 721.5025019044709 00:00:00 00:00:00 0.9018979316139177 20.893413876660702 -1.0 -1.0 -1.0 35.0 888 288 +34 1087.4264164816361 00:00:00 00:00:00 1.035096011417865 12.559979217082482 -1.0 -1.0 -1.0 35.0 888 288 +35 163.23187688161033 00:00:00 00:00:00 0.1333990348830121 78.61411210143966 -1.0 -1.0 -1.0 35.0 888 288 +36 515.446178879044 00:00:00 00:00:00 0.7752482941845488 11.417411530656945 -1.0 -1.0 -1.0 35.0 888 288 +37 392.6137583987218 00:00:00 00:00:00 0.31949791108182546 16.89212239318249 -1.0 -1.0 -1.0 35.0 888 288 +38 719.6387498724689 00:00:00 00:00:00 0.9168854467146328 22.483314792466093 -1.0 -1.0 -1.0 35.0 888 288 +39 1095.9625583277343 00:00:00 00:00:00 1.3932609853721545 13.438743382703526 -1.0 -1.0 -1.0 35.0 888 288 +40 758.3297416501492 00:00:00 00:00:00 0.6401398012876773 12.85406115188814 -1.0 -1.0 -1.0 35.0 888 288 +41 196.42239794857238 00:00:00 00:00:00 0.15281141706879714 36.32216074676754 -1.0 -1.0 -1.0 35.0 888 288 +42 300.9638203550653 00:00:00 00:00:00 0.38656404639181324 13.974445331317181 -1.0 -1.0 -1.0 35.0 888 288 +43 819.1568769824953 00:00:00 00:00:00 0.8538690229315276 10.172610101290998 -1.0 -1.0 -1.0 35.0 888 288 +44 672.8493862791563 00:00:00 00:00:00 0.6796843371842363 13.890953706380596 -1.0 -1.0 -1.0 35.0 888 288 +45 171.9322515506212 00:00:00 00:00:00 0.151862894364487 34.7413812643017 -1.0 -1.0 -1.0 35.0 888 288 +46 1425.9053080008052 00:00:00 00:00:00 1.7591365750119778 12.7456387947933 -1.0 -1.0 -1.0 35.0 888 288 +47 489.25638240698544 00:00:00 00:00:00 0.5271052638239554 16.173416507939116 -1.0 -1.0 -1.0 35.0 888 288 +48 303.6841850164093 00:00:00 00:00:00 0.25795471657648067 10.375992085957462 -1.0 -1.0 -1.0 35.0 888 288 +49 875.3662532129479 00:00:00 00:00:00 0.6415497100158352 10.214362638300399 -1.0 -1.0 -1.0 35.0 888 288 +50 716.6818881006753 00:00:00 00:00:00 0.6864110893626921 14.452157966062835 -1.0 -1.0 -1.0 35.0 888 288 +51 1780.2827105839892 00:00:00 00:00:00 1.3436702111751808 10.3884232109404 -1.0 -1.0 -1.0 35.0 888 288 +52 996.2724896444047 00:00:00 00:00:00 0.5216743407822534 13.213225298728211 -1.0 -1.0 -1.0 35.0 888 288 +53 1698.7961537425167 00:00:00 00:00:00 1.5613492775221662 12.607503410081835 -1.0 -1.0 -1.0 35.0 888 288 +54 1126.5413295499866 00:00:00 00:00:00 1.0336520109034335 17.051568734514 -1.0 -1.0 -1.0 35.0 888 288 +55 450.88973429474333 00:00:00 00:00:00 0.36252077841437375 10.69280211941846 -1.0 -1.0 -1.0 35.0 888 288 +56 973.3753477739596 00:00:00 00:00:00 1.1828217991774477 17.506572347460136 -1.0 -1.0 -1.0 35.0 888 288 +57 1489.8174891533963 00:00:00 00:00:00 0.46010099973404217 52.449273584894264 -1.0 -1.0 -1.0 35.0 888 288 +58 884.0458853234747 00:00:00 00:00:00 0.94457312363897 15.638104651466023 -1.0 -1.0 -1.0 35.0 888 288 +59 1371.7618432631962 00:00:00 00:00:00 1.4252404386072601 11.871898609476887 -1.0 -1.0 -1.0 35.0 888 288 +60 779.9123200563506 00:00:00 00:00:00 0.7695273462562608 11.377991388220451 -1.0 -1.0 -1.0 35.0 888 288 +61 244.32835931715942 00:00:00 00:00:00 0.1512188351440696 22.08068896327773 -1.0 -1.0 -1.0 35.0 888 288 +62 170.42643162915402 00:00:00 00:00:00 0.15963831683903665 11.891868553016314 -1.0 -1.0 -1.0 35.0 888 288 +63 697.6397584685641 00:00:00 00:00:00 0.6155151419755035 10.141332615940971 -1.0 -1.0 -1.0 35.0 888 288 +64 896.3411776022986 00:00:00 00:00:00 0.83352003928136 13.956903508254495 -1.0 -1.0 -1.0 35.0 888 288 +65 673.4550793235062 00:00:00 00:00:00 0.5008895818994006 34.96309565327556 -1.0 -1.0 -1.0 35.0 888 288 +66 409.3504561455062 00:00:00 00:00:00 0.38651409263865677 17.073302923618904 -1.0 -1.0 -1.0 35.0 888 288 +67 1279.5121634320094 00:00:00 00:00:00 1.766754211557735 12.06090823054981 -1.0 -1.0 -1.0 35.0 888 288 +68 455.1932713332237 00:00:00 00:00:00 0.7629777494045573 13.581352455460024 -1.0 -1.0 -1.0 35.0 888 288 +69 212.5080624683044 00:00:00 00:00:00 0.24562999100714805 72.07735525486979 -1.0 -1.0 -1.0 35.0 888 288 +70 208.83082789773542 00:00:00 00:00:00 0.09345708729049836 35.86335125977792 -1.0 -1.0 -1.0 35.0 888 288 +71 218.52612475416436 00:00:00 00:00:00 0.2169993110272826 30.226196813374152 -1.0 -1.0 -1.0 35.0 888 288 +72 1591.9222149862642 00:00:00 00:00:00 1.8072297925518148 12.417298253806685 -1.0 -1.0 -1.0 35.0 888 288 +73 218.79115225062986 00:00:00 00:00:00 0.031212774818377767 14.217908248312149 -1.0 -1.0 -1.0 35.0 888 288 +74 618.4593505087528 00:00:00 00:00:00 0.7560975114810532 11.664308801315117 -1.0 -1.0 -1.0 35.0 888 288 +75 652.0130715223407 00:00:00 00:00:00 0.7242255489992683 12.468451895755134 -1.0 -1.0 -1.0 35.0 888 288 +76 673.0857403351645 00:00:00 00:00:00 0.9253391770587509 14.050855469722556 -1.0 -1.0 -1.0 35.0 888 288 +77 785.3518302003876 00:00:00 00:00:00 1.1363130575887341 11.213356812360049 -1.0 -1.0 -1.0 35.0 888 288 +78 921.812608616067 00:00:00 00:00:00 0.5308587220894548 10.282098860310228 -1.0 -1.0 -1.0 35.0 888 288 +79 656.9337821238685 00:00:00 00:00:00 0.7718799884749266 11.233631650878838 -1.0 -1.0 -1.0 35.0 888 288 +80 1138.704218112882 00:00:00 00:00:00 1.4103564032980764 26.539797617550633 -1.0 -1.0 -1.0 35.0 888 288 +81 620.5653235040173 00:00:00 00:00:00 0.3608403500538981 21.45481401537115 -1.0 -1.0 -1.0 35.0 888 288 +82 838.2486260752073 00:00:00 00:00:00 0.9692149421266859 10.446614306626344 -1.0 -1.0 -1.0 35.0 888 288 +83 552.5443734504142 00:00:00 00:00:00 0.48084252190706855 21.543404977178355 -1.0 -1.0 -1.0 35.0 888 288 +84 573.8507253223063 00:00:00 00:00:00 0.48740732199390757 11.084164653890928 -1.0 -1.0 -1.0 35.0 888 288 +85 420.6998183657156 00:00:00 00:00:00 0.44320207044782256 17.32850048985882 -1.0 -1.0 -1.0 35.0 888 288 +86 439.5439082870578 00:00:00 00:00:00 0.2783782253902463 35.833665311539534 -1.0 -1.0 -1.0 35.0 888 288 +87 1130.0482166914742 00:00:00 00:00:00 1.2831967999076628 12.35679852892303 -1.0 -1.0 -1.0 35.0 888 288 +88 450.20877052020955 00:00:00 00:00:00 0.19926746916562046 12.665759219918368 -1.0 -1.0 -1.0 35.0 888 288 +89 110.1657516020332 00:00:00 00:00:00 0.1195172001672582 18.200353247030144 -1.0 -1.0 -1.0 35.0 888 288 +90 443.8604241510417 00:00:00 00:00:00 0.33340333195524796 13.242335799793306 -1.0 -1.0 -1.0 35.0 888 288 +91 444.686556791516 00:00:00 00:00:00 0.4191471059154911 13.80369034602879 -1.0 -1.0 -1.0 35.0 888 288 +92 562.5781184397683 00:00:00 00:00:00 0.680176372049107 10.27995831481763 -1.0 -1.0 -1.0 35.0 888 288 +93 167.62653574722364 00:00:00 00:00:00 0.0389571382922253 17.570916297751136 -1.0 -1.0 -1.0 35.0 888 288 +94 481.62428400668705 00:00:00 00:00:00 0.48270894964902716 11.826070143713975 -1.0 -1.0 -1.0 35.0 888 288 +95 475.5794761937849 00:00:00 00:00:00 0.5935539854869426 12.987091653999931 -1.0 -1.0 -1.0 35.0 888 288 +96 295.3711932454349 00:00:00 00:00:00 0.2618277155954746 11.871291318754192 -1.0 -1.0 -1.0 35.0 888 288 +97 1364.3326396162597 00:00:00 00:00:00 1.5749185090159343 10.696711455581303 -1.0 -1.0 -1.0 35.0 888 288 +98 584.9391974583868 00:00:00 00:00:00 0.6519095988358196 10.372529937559637 -1.0 -1.0 -1.0 35.0 888 288 +99 1076.886030040255 00:00:00 00:00:00 1.023384185705521 14.901426606089192 -1.0 -1.0 -1.0 35.0 888 288 diff --git a/papers/lsst/Photometric/Smeared_and_zFrac.ecsv b/papers/lsst/Photometric/Smeared_and_zFrac.ecsv index 5efc1561..5d0c95a4 100644 --- a/papers/lsst/Photometric/Smeared_and_zFrac.ecsv +++ b/papers/lsst/Photometric/Smeared_and_zFrac.ecsv @@ -14,93 +14,82 @@ # - {name: FBAR, datatype: float64} # - {name: BW, datatype: float64} # meta: !!omap -# - {survey_data: '{"observing": {"NORM_FRB": 17,"TOBS": 64.68,"MAX_IW": 8, "MAXWMETH": 2}, +# - {survey_data: '{"observing": {"NORM_FRB": 17,"TOBS": 64.68,"MAX_IW": 8, "MAXWMETH": 2, "Z_FRACTION": 24.7, "Z_PHOTO": 0.03}, # "telescope": {"BEAM": "CRACO_900", "DMMASK": "craco_900_mask.npy", # "DIAM": 12.0, "NBEAMS": 1, "NBINS": 5, "FBAR": 906, # "TRES": 13.8, "FRES": 1.0, "THRESH": 1.01}}'} TNS DM RA DEC Z SNR WIDTH Gl Gb DMG FBAR BW -1 1293.8332446347993 00:00:00 00:00:00 0.8026726589177778 12.346871755650161 -1.0 -1.0 -1.0 35.0 888 288 -5 789.2339950025221 00:00:00 00:00:00 0.22440979482747994 15.974081920078653 -1.0 -1.0 -1.0 35.0 888 288 -6 322.2960237321481 00:00:00 00:00:00 0.30011338886437156 41.61528388827413 -1.0 -1.0 -1.0 35.0 888 288 -7 291.26523564494755 00:00:00 00:00:00 0.3716353845205117 35.39286498287345 -1.0 -1.0 -1.0 35.0 888 288 -8 265.6172817387779 00:00:00 00:00:00 0.07031358571327158 17.005499594805812 -1.0 -1.0 -1.0 35.0 888 288 -9 837.3735197561044 00:00:00 00:00:00 0.6925267387932372 22.368861955427324 -1.0 -1.0 -1.0 35.0 888 288 -10 430.42288234797786 00:00:00 00:00:00 0.5453395634766126 31.14684691070062 -1.0 -1.0 -1.0 35.0 888 288 -11 338.8720812210228 00:00:00 00:00:00 0.4757401036066104 11.514360186435827 -1.0 -1.0 -1.0 35.0 888 288 -12 459.8929794701774 00:00:00 00:00:00 0.6111553250126587 28.970291709672008 -1.0 -1.0 -1.0 35.0 888 288 -13 514.2392669873378 00:00:00 00:00:00 0.48320447158887225 26.23234752429593 -1.0 -1.0 -1.0 35.0 888 288 -14 444.82316308571853 00:00:00 00:00:00 0.16778987448479044 10.224780135231132 -1.0 -1.0 -1.0 35.0 888 288 -15 423.4287617724789 00:00:00 00:00:00 0.12667007201296906 16.93166467819824 -1.0 -1.0 -1.0 35.0 888 288 -16 250.18714908014232 00:00:00 00:00:00 0.1591634140141581 16.825688142584564 -1.0 -1.0 -1.0 35.0 888 288 -17 230.04604721703424 00:00:00 00:00:00 0.20900964741015637 30.06214700360865 -1.0 -1.0 -1.0 35.0 888 288 -18 620.1606829061071 00:00:00 00:00:00 0.3354237408317748 48.42640123698046 -1.0 -1.0 -1.0 35.0 888 288 -19 548.9677620404942 00:00:00 00:00:00 0.16933329054962168 14.25071741051486 -1.0 -1.0 -1.0 35.0 888 288 -20 208.45270441661265 00:00:00 00:00:00 0.2175794326593667 11.795177754311347 -1.0 -1.0 -1.0 35.0 888 288 -22 454.31237113237376 00:00:00 00:00:00 0.3285218412623604 14.83980914864711 -1.0 -1.0 -1.0 35.0 888 288 -23 188.43753414368308 00:00:00 00:00:00 0.1006293027745211 26.190444971253193 -1.0 -1.0 -1.0 35.0 888 288 -24 432.25074329840373 00:00:00 00:00:00 0.43820309825233555 46.827196897045056 -1.0 -1.0 -1.0 35.0 888 288 -25 579.5866100250605 00:00:00 00:00:00 0.8228428464865679 15.689862449648482 -1.0 -1.0 -1.0 35.0 888 288 -26 5036.7746606005 00:00:00 00:00:00 0.4655133704578701 11.894270840857446 -1.0 -1.0 -1.0 35.0 888 288 -27 143.49252810767797 00:00:00 00:00:00 0.04138013323734881 484.69318911542297 -1.0 -1.0 -1.0 35.0 888 288 -29 695.548826635149 00:00:00 00:00:00 0.6072055708319989 12.325923688964098 -1.0 -1.0 -1.0 35.0 888 288 -30 518.6973214909851 00:00:00 00:00:00 0.1858292681019909 40.622240538994284 -1.0 -1.0 -1.0 35.0 888 288 -31 820.101280852925 00:00:00 00:00:00 1.1907785396605095 11.906421530120177 -1.0 -1.0 -1.0 35.0 888 288 -32 805.6084061258246 00:00:00 00:00:00 0.2539606628978965 11.353905426206259 -1.0 -1.0 -1.0 35.0 888 288 -33 419.29611869030475 00:00:00 00:00:00 0.15958310375490395 16.60160673523299 -1.0 -1.0 -1.0 35.0 888 288 -34 155.6073504242375 00:00:00 00:00:00 0.14762739361862726 66.13285676320632 -1.0 -1.0 -1.0 35.0 888 288 -35 1147.6509354631635 00:00:00 00:00:00 1.0196449201386366 10.989377138257774 -1.0 -1.0 -1.0 35.0 888 288 -36 434.9054218021994 00:00:00 00:00:00 0.36587030490427747 238.5088838108578 -1.0 -1.0 -1.0 35.0 888 288 -37 310.16626884466916 00:00:00 00:00:00 0.18569892608759028 14.360447206185484 -1.0 -1.0 -1.0 35.0 888 288 -38 257.9624832084242 00:00:00 00:00:00 0.18154696874743803 21.270338041223077 -1.0 -1.0 -1.0 35.0 888 288 -40 387.26342412555556 00:00:00 00:00:00 0.5271999423649261 61.00023532034031 -1.0 -1.0 -1.0 35.0 888 288 -41 275.28452072976125 00:00:00 00:00:00 0.09048525492849627 48.83811893549135 -1.0 -1.0 -1.0 35.0 888 288 -42 1067.4327278110532 00:00:00 00:00:00 0.08708632585333373 20.27598540262997 -1.0 -1.0 -1.0 35.0 888 288 -43 730.1731714209269 00:00:00 00:00:00 0.44597961391510366 11.312977703982753 -1.0 -1.0 -1.0 35.0 888 288 -44 879.3689116020875 00:00:00 00:00:00 0.6714204031929973 20.44890692681992 -1.0 -1.0 -1.0 35.0 888 288 -45 140.53262627190654 00:00:00 00:00:00 0.09784407401506416 12.864108327165525 -1.0 -1.0 -1.0 35.0 888 288 -46 138.60386510840408 00:00:00 00:00:00 0.11280585785856893 67.09929347610452 -1.0 -1.0 -1.0 35.0 888 288 -47 457.39367798062347 00:00:00 00:00:00 0.3400258623394666 126.10472883527777 -1.0 -1.0 -1.0 35.0 888 288 -48 720.9874661585104 00:00:00 00:00:00 0.649765717228654 14.19983264302042 -1.0 -1.0 -1.0 35.0 888 288 -49 272.4500208102785 00:00:00 00:00:00 0.13769590971746024 23.153353203332813 -1.0 -1.0 -1.0 35.0 888 288 -50 229.67723852719107 00:00:00 00:00:00 0.12006320865875231 15.939674250127918 -1.0 -1.0 -1.0 35.0 888 288 -51 331.58825171496477 00:00:00 00:00:00 0.25698668054291335 35.76985881730512 -1.0 -1.0 -1.0 35.0 888 288 -53 491.54267256019284 00:00:00 00:00:00 0.4270363735922269 15.873128885187782 -1.0 -1.0 -1.0 35.0 888 288 -54 529.128624543456 00:00:00 00:00:00 0.13467652357181403 30.946910009847507 -1.0 -1.0 -1.0 35.0 888 288 -55 399.1055744918342 00:00:00 00:00:00 0.2859800906559676 10.88583325549367 -1.0 -1.0 -1.0 35.0 888 288 -56 431.0128668027263 00:00:00 00:00:00 0.3378338742731474 10.468990242397023 -1.0 -1.0 -1.0 35.0 888 288 -57 190.52132889218325 00:00:00 00:00:00 0.17799965496545547 16.66318259472948 -1.0 -1.0 -1.0 35.0 888 288 -58 113.15491238877307 00:00:00 00:00:00 0.039021369275481325 52.02458333198281 -1.0 -1.0 -1.0 35.0 888 288 -59 559.3752828715894 00:00:00 00:00:00 0.6988831678653942 21.37087749061021 -1.0 -1.0 -1.0 35.0 888 288 -61 532.6898941820887 00:00:00 00:00:00 0.18621399996785953 29.232208234528766 -1.0 -1.0 -1.0 35.0 888 288 -64 622.5786144978405 00:00:00 00:00:00 0.2325455138564069 86.80356920697237 -1.0 -1.0 -1.0 35.0 888 288 -65 691.7803873564447 00:00:00 00:00:00 0.5529703609657931 12.899063417967223 -1.0 -1.0 -1.0 35.0 888 288 -66 425.79471677447805 00:00:00 00:00:00 0.5556620352238183 18.002963736786747 -1.0 -1.0 -1.0 35.0 888 288 -68 414.8587456661204 00:00:00 00:00:00 0.310070672785909 10.944184015097571 -1.0 -1.0 -1.0 35.0 888 288 -69 663.4430327655946 00:00:00 00:00:00 0.731575569293054 30.236972085102074 -1.0 -1.0 -1.0 35.0 888 288 -70 148.69756109291183 00:00:00 00:00:00 0.10085726227990287 86.90035422142955 -1.0 -1.0 -1.0 35.0 888 288 -72 152.9383064815016 00:00:00 00:00:00 0.06045578514048208 40.184792633110376 -1.0 -1.0 -1.0 35.0 888 288 -74 197.05046287007687 00:00:00 00:00:00 0.3250564943880785 11.866829353300446 -1.0 -1.0 -1.0 35.0 888 288 -75 1152.0351297397929 00:00:00 00:00:00 1.2772288625892987 22.166897711183683 -1.0 -1.0 -1.0 35.0 888 288 -76 574.7793210141199 00:00:00 00:00:00 0.15837600883038636 19.45858882312784 -1.0 -1.0 -1.0 35.0 888 288 -77 1001.7697964755914 00:00:00 00:00:00 1.0036542633334602 12.261260069068786 -1.0 -1.0 -1.0 35.0 888 288 -78 155.39833051180779 00:00:00 00:00:00 0.19347898107465472 15.413819616047661 -1.0 -1.0 -1.0 35.0 888 288 -79 721.4914716203449 00:00:00 00:00:00 0.30295383251025304 15.099468387889068 -1.0 -1.0 -1.0 35.0 888 288 -80 544.2632638190482 00:00:00 00:00:00 0.25848937308392594 16.937293035041506 -1.0 -1.0 -1.0 35.0 888 288 -81 413.7706136774752 00:00:00 00:00:00 0.3861876593421423 45.53821522890108 -1.0 -1.0 -1.0 35.0 888 288 -82 512.2968332797499 00:00:00 00:00:00 0.19186009644515764 65.41049706254961 -1.0 -1.0 -1.0 35.0 888 288 -83 604.6614773741395 00:00:00 00:00:00 0.5922496204950911 87.47806150668559 -1.0 -1.0 -1.0 35.0 888 288 -84 1518.9511344791895 00:00:00 00:00:00 0.5221800070467364 21.702413338435306 -1.0 -1.0 -1.0 35.0 888 288 -85 643.7854589158669 00:00:00 00:00:00 0.20310544997982768 10.123746839702076 -1.0 -1.0 -1.0 35.0 888 288 -86 126.27385897827892 00:00:00 00:00:00 0.14890248371153225 14.943311830534453 -1.0 -1.0 -1.0 35.0 888 288 -88 108.5822318161769 00:00:00 00:00:00 0.004906613899604562 98.37197874300217 -1.0 -1.0 -1.0 35.0 888 288 -89 454.4376843789279 00:00:00 00:00:00 0.1806707830480725 11.962983358961118 -1.0 -1.0 -1.0 35.0 888 288 -90 431.94315540593266 00:00:00 00:00:00 0.44017691427265276 46.19921231942614 -1.0 -1.0 -1.0 35.0 888 288 -91 563.7835858233838 00:00:00 00:00:00 0.5942396002726046 12.142815139562412 -1.0 -1.0 -1.0 35.0 888 288 -92 337.3005917385242 00:00:00 00:00:00 0.4810711931332526 14.345395179118563 -1.0 -1.0 -1.0 35.0 888 288 -93 147.4661489599992 00:00:00 00:00:00 -0.014431551553354828 504.1131760754106 -1.0 -1.0 -1.0 35.0 888 288 -94 358.8683383314342 00:00:00 00:00:00 0.3062464185841798 18.326073205181974 -1.0 -1.0 -1.0 35.0 888 288 -95 326.5453849076442 00:00:00 00:00:00 0.4124803983720404 12.654073325103571 -1.0 -1.0 -1.0 35.0 888 288 -96 427.76775974005335 00:00:00 00:00:00 0.2810773811596752 25.75055977593358 -1.0 -1.0 -1.0 35.0 888 288 -97 1537.250617119628 00:00:00 00:00:00 0.8858851936214186 16.729141697437374 -1.0 -1.0 -1.0 35.0 888 288 -98 463.1233303504703 00:00:00 00:00:00 0.5140920482601581 11.12631974466657 -1.0 -1.0 -1.0 35.0 888 288 -99 323.04706650622984 00:00:00 00:00:00 0.23990636996638962 44.49101643122364 -1.0 -1.0 -1.0 35.0 888 288 +1 616.5738602258931 00:00:00 00:00:00 0.8349135358303849 10.160405042320036 -1.0 -1.0 -1.0 35.0 888 288 +2 946.6052448796315 00:00:00 00:00:00 0.6474993506647525 10.652772181822607 -1.0 -1.0 -1.0 35.0 888 288 +3 586.5727013781997 00:00:00 00:00:00 0.12495187173178796 12.415341138097366 -1.0 -1.0 -1.0 35.0 888 288 +4 2153.3992719321486 00:00:00 00:00:00 0.8036468331892808 12.24289806494758 -1.0 -1.0 -1.0 35.0 888 288 +6 529.8895060996854 00:00:00 00:00:00 0.603211700191891 13.133173041616157 -1.0 -1.0 -1.0 35.0 888 288 +8 412.87460299855934 00:00:00 00:00:00 0.21865062413724176 12.846680716802911 -1.0 -1.0 -1.0 35.0 888 288 +9 701.6217045820609 00:00:00 00:00:00 0.7299571477022802 10.341966201772214 -1.0 -1.0 -1.0 35.0 888 288 +10 205.82201397136845 00:00:00 00:00:00 0.06074866029287343 20.941567140489564 -1.0 -1.0 -1.0 35.0 888 288 +12 275.5937204880448 00:00:00 00:00:00 0.09245951767772358 91.38695892704916 -1.0 -1.0 -1.0 35.0 888 288 +13 436.349204138444 00:00:00 00:00:00 0.44564144183478205 15.275450861578175 -1.0 -1.0 -1.0 35.0 888 288 +14 295.6406494853049 00:00:00 00:00:00 0.1442212477744424 11.219409175951904 -1.0 -1.0 -1.0 35.0 888 288 +15 291.6471440894715 00:00:00 00:00:00 0.2990568845451625 11.18814447805394 -1.0 -1.0 -1.0 35.0 888 288 +16 1618.8558318442904 00:00:00 00:00:00 0.9448819250948836 24.749670081652773 -1.0 -1.0 -1.0 35.0 888 288 +17 983.9563468218174 00:00:00 00:00:00 0.5110387741801308 12.591584618769005 -1.0 -1.0 -1.0 35.0 888 288 +19 645.2399497353754 00:00:00 00:00:00 0.49888042615577133 10.286082085018421 -1.0 -1.0 -1.0 35.0 888 288 +20 539.5953081699104 00:00:00 00:00:00 0.6182169860530876 35.45318302068964 -1.0 -1.0 -1.0 35.0 888 288 +21 443.6632526190763 00:00:00 00:00:00 0.596702703542215 14.319328661152863 -1.0 -1.0 -1.0 35.0 888 288 +22 81.45567951589115 00:00:00 00:00:00 0.06896887062626902 15.566080612070119 -1.0 -1.0 -1.0 35.0 888 288 +23 374.91893100970947 00:00:00 00:00:00 0.08654025153807617 29.766613679561118 -1.0 -1.0 -1.0 35.0 888 288 +25 711.626862396593 00:00:00 00:00:00 0.14070511599819086 13.229440860953233 -1.0 -1.0 -1.0 35.0 888 288 +26 356.4667997984884 00:00:00 00:00:00 0.44644296786158993 20.3929051321851 -1.0 -1.0 -1.0 35.0 888 288 +27 405.6849410353128 00:00:00 00:00:00 0.43892086227384686 12.020783061083032 -1.0 -1.0 -1.0 35.0 888 288 +28 438.56039965043755 00:00:00 00:00:00 0.41899323244205144 14.962222736067005 -1.0 -1.0 -1.0 35.0 888 288 +29 1032.0031340800927 00:00:00 00:00:00 1.0111782328413856 12.249965125983609 -1.0 -1.0 -1.0 35.0 888 288 +30 217.45075943955328 00:00:00 00:00:00 0.18461768474454746 13.329278119983234 -1.0 -1.0 -1.0 35.0 888 288 +31 337.87773894962976 00:00:00 00:00:00 0.3539648344531408 15.842464362846469 -1.0 -1.0 -1.0 35.0 888 288 +32 747.9605061576287 00:00:00 00:00:00 0.40398282515571504 11.35266012864496 -1.0 -1.0 -1.0 35.0 888 288 +35 163.23187688161033 00:00:00 00:00:00 0.1333990348830121 78.61411210143966 -1.0 -1.0 -1.0 35.0 888 288 +37 392.6137583987218 00:00:00 00:00:00 0.31949791108182546 16.89212239318249 -1.0 -1.0 -1.0 35.0 888 288 +40 758.3297416501492 00:00:00 00:00:00 0.6401398012876773 12.85406115188814 -1.0 -1.0 -1.0 35.0 888 288 +41 196.42239794857238 00:00:00 00:00:00 0.15281141706879714 36.32216074676754 -1.0 -1.0 -1.0 35.0 888 288 +42 300.9638203550653 00:00:00 00:00:00 0.38656404639181324 13.974445331317181 -1.0 -1.0 -1.0 35.0 888 288 +43 819.1568769824953 00:00:00 00:00:00 0.8538690229315276 10.172610101290998 -1.0 -1.0 -1.0 35.0 888 288 +45 171.9322515506212 00:00:00 00:00:00 0.151862894364487 34.7413812643017 -1.0 -1.0 -1.0 35.0 888 288 +47 489.25638240698544 00:00:00 00:00:00 0.5271052638239554 16.173416507939116 -1.0 -1.0 -1.0 35.0 888 288 +48 303.6841850164093 00:00:00 00:00:00 0.25795471657648067 10.375992085957462 -1.0 -1.0 -1.0 35.0 888 288 +49 875.3662532129479 00:00:00 00:00:00 0.6415497100158352 10.214362638300399 -1.0 -1.0 -1.0 35.0 888 288 +50 716.6818881006753 00:00:00 00:00:00 0.6864110893626921 14.452157966062835 -1.0 -1.0 -1.0 35.0 888 288 +51 1780.2827105839892 00:00:00 00:00:00 1.3436702111751808 10.3884232109404 -1.0 -1.0 -1.0 35.0 888 288 +52 996.2724896444047 00:00:00 00:00:00 0.5216743407822534 13.213225298728211 -1.0 -1.0 -1.0 35.0 888 288 +54 1126.5413295499866 00:00:00 00:00:00 1.0336520109034335 17.051568734514 -1.0 -1.0 -1.0 35.0 888 288 +55 450.88973429474333 00:00:00 00:00:00 0.36252077841437375 10.69280211941846 -1.0 -1.0 -1.0 35.0 888 288 +57 1489.8174891533963 00:00:00 00:00:00 0.46010099973404217 52.449273584894264 -1.0 -1.0 -1.0 35.0 888 288 +61 244.32835931715942 00:00:00 00:00:00 0.1512188351440696 22.08068896327773 -1.0 -1.0 -1.0 35.0 888 288 +62 170.42643162915402 00:00:00 00:00:00 0.15963831683903665 11.891868553016314 -1.0 -1.0 -1.0 35.0 888 288 +64 896.3411776022986 00:00:00 00:00:00 0.83352003928136 13.956903508254495 -1.0 -1.0 -1.0 35.0 888 288 +65 673.4550793235062 00:00:00 00:00:00 0.5008895818994006 34.96309565327556 -1.0 -1.0 -1.0 35.0 888 288 +66 409.3504561455062 00:00:00 00:00:00 0.38651409263865677 17.073302923618904 -1.0 -1.0 -1.0 35.0 888 288 +68 455.1932713332237 00:00:00 00:00:00 0.7629777494045573 13.581352455460024 -1.0 -1.0 -1.0 35.0 888 288 +69 212.5080624683044 00:00:00 00:00:00 0.24562999100714805 72.07735525486979 -1.0 -1.0 -1.0 35.0 888 288 +70 208.83082789773542 00:00:00 00:00:00 0.09345708729049836 35.86335125977792 -1.0 -1.0 -1.0 35.0 888 288 +71 218.52612475416436 00:00:00 00:00:00 0.2169993110272826 30.226196813374152 -1.0 -1.0 -1.0 35.0 888 288 +73 218.79115225062986 00:00:00 00:00:00 0.031212774818377767 14.217908248312149 -1.0 -1.0 -1.0 35.0 888 288 +74 618.4593505087528 00:00:00 00:00:00 0.7560975114810532 11.664308801315117 -1.0 -1.0 -1.0 35.0 888 288 +75 652.0130715223407 00:00:00 00:00:00 0.7242255489992683 12.468451895755134 -1.0 -1.0 -1.0 35.0 888 288 +76 673.0857403351645 00:00:00 00:00:00 0.9253391770587509 14.050855469722556 -1.0 -1.0 -1.0 35.0 888 288 +79 656.9337821238685 00:00:00 00:00:00 0.7718799884749266 11.233631650878838 -1.0 -1.0 -1.0 35.0 888 288 +80 1138.704218112882 00:00:00 00:00:00 1.4103564032980764 26.539797617550633 -1.0 -1.0 -1.0 35.0 888 288 +81 620.5653235040173 00:00:00 00:00:00 0.3608403500538981 21.45481401537115 -1.0 -1.0 -1.0 35.0 888 288 +83 552.5443734504142 00:00:00 00:00:00 0.48084252190706855 21.543404977178355 -1.0 -1.0 -1.0 35.0 888 288 +84 573.8507253223063 00:00:00 00:00:00 0.48740732199390757 11.084164653890928 -1.0 -1.0 -1.0 35.0 888 288 +85 420.6998183657156 00:00:00 00:00:00 0.44320207044782256 17.32850048985882 -1.0 -1.0 -1.0 35.0 888 288 +86 439.5439082870578 00:00:00 00:00:00 0.2783782253902463 35.833665311539534 -1.0 -1.0 -1.0 35.0 888 288 +88 450.20877052020955 00:00:00 00:00:00 0.19926746916562046 12.665759219918368 -1.0 -1.0 -1.0 35.0 888 288 +89 110.1657516020332 00:00:00 00:00:00 0.1195172001672582 18.200353247030144 -1.0 -1.0 -1.0 35.0 888 288 +90 443.8604241510417 00:00:00 00:00:00 0.33340333195524796 13.242335799793306 -1.0 -1.0 -1.0 35.0 888 288 +91 444.686556791516 00:00:00 00:00:00 0.4191471059154911 13.80369034602879 -1.0 -1.0 -1.0 35.0 888 288 +92 562.5781184397683 00:00:00 00:00:00 0.680176372049107 10.27995831481763 -1.0 -1.0 -1.0 35.0 888 288 +93 167.62653574722364 00:00:00 00:00:00 0.0389571382922253 17.570916297751136 -1.0 -1.0 -1.0 35.0 888 288 +94 481.62428400668705 00:00:00 00:00:00 0.48270894964902716 11.826070143713975 -1.0 -1.0 -1.0 35.0 888 288 +95 475.5794761937849 00:00:00 00:00:00 0.5935539854869426 12.987091653999931 -1.0 -1.0 -1.0 35.0 888 288 +96 295.3711932454349 00:00:00 00:00:00 0.2618277155954746 11.871291318754192 -1.0 -1.0 -1.0 35.0 888 288 +98 584.9391974583868 00:00:00 00:00:00 0.6519095988358196 10.372529937559637 -1.0 -1.0 -1.0 35.0 888 288 +99 1076.886030040255 00:00:00 00:00:00 1.023384185705521 14.901426606089192 -1.0 -1.0 -1.0 35.0 888 288 diff --git a/papers/lsst/Photometric/Spectroscopic.ecsv b/papers/lsst/Photometric/Spectroscopic.ecsv index 5f1b28d2..92589bc5 100644 --- a/papers/lsst/Photometric/Spectroscopic.ecsv +++ b/papers/lsst/Photometric/Spectroscopic.ecsv @@ -19,103 +19,103 @@ # "DIAM": 12.0, "NBEAMS": 1, "NBINS": 5, "FBAR": 906, # "TRES": 13.8, "FRES": 1.0, "THRESH": 1.01}}'} TNS DM RA DEC Z SNR WIDTH Gl Gb DMG FBAR BW -0 2334.5710018337304 00:00:00 00:00:00 3.038446245408985 42.9076170576045 -1.0 -1.0 -1.0 35.0 888 288 -1 1293.8332446347993 00:00:00 00:00:00 0.8413293511214376 12.346871755650161 -1.0 -1.0 -1.0 35.0 888 288 -2 1130.9791692249696 00:00:00 00:00:00 0.6589650136717322 16.45799179528472 -1.0 -1.0 -1.0 35.0 888 288 -3 948.9962810274221 00:00:00 00:00:00 1.0629417961000824 12.899405096865053 -1.0 -1.0 -1.0 35.0 888 288 -4 3302.7414319309637 00:00:00 00:00:00 3.4842119213365517 10.896087814782916 -1.0 -1.0 -1.0 35.0 888 288 -5 789.2339950025221 00:00:00 00:00:00 0.25506480932174347 15.974081920078653 -1.0 -1.0 -1.0 35.0 888 288 -6 322.2960237321481 00:00:00 00:00:00 0.27128843344528 41.61528388827413 -1.0 -1.0 -1.0 35.0 888 288 -7 291.26523564494755 00:00:00 00:00:00 0.4466332273656116 35.39286498287345 -1.0 -1.0 -1.0 35.0 888 288 -8 265.6172817387779 00:00:00 00:00:00 0.18436196594727836 17.005499594805812 -1.0 -1.0 -1.0 35.0 888 288 -9 837.3735197561044 00:00:00 00:00:00 0.6950751437047783 22.368861955427324 -1.0 -1.0 -1.0 35.0 888 288 -10 430.42288234797786 00:00:00 00:00:00 0.556902505194651 31.14684691070062 -1.0 -1.0 -1.0 35.0 888 288 -11 338.8720812210228 00:00:00 00:00:00 0.4188292757070367 11.514360186435827 -1.0 -1.0 -1.0 35.0 888 288 -12 459.8929794701774 00:00:00 00:00:00 0.5903928193481528 28.970291709672008 -1.0 -1.0 -1.0 35.0 888 288 -13 514.2392669873378 00:00:00 00:00:00 0.4823674377490287 26.23234752429593 -1.0 -1.0 -1.0 35.0 888 288 -14 444.82316308571853 00:00:00 00:00:00 0.18093771758703725 10.224780135231132 -1.0 -1.0 -1.0 35.0 888 288 -15 423.4287617724789 00:00:00 00:00:00 0.1281487760475812 16.93166467819824 -1.0 -1.0 -1.0 35.0 888 288 -16 250.18714908014232 00:00:00 00:00:00 0.14584265666328355 16.825688142584564 -1.0 -1.0 -1.0 35.0 888 288 -17 230.04604721703424 00:00:00 00:00:00 0.22897196618507287 30.06214700360865 -1.0 -1.0 -1.0 35.0 888 288 -18 620.1606829061071 00:00:00 00:00:00 0.32878933144592676 48.42640123698046 -1.0 -1.0 -1.0 35.0 888 288 -19 548.9677620404942 00:00:00 00:00:00 0.205349720070631 14.25071741051486 -1.0 -1.0 -1.0 35.0 888 288 -20 208.45270441661265 00:00:00 00:00:00 0.15446192559965205 11.795177754311347 -1.0 -1.0 -1.0 35.0 888 288 -21 1456.3273969505499 00:00:00 00:00:00 1.1381431565978972 15.265200782969 -1.0 -1.0 -1.0 35.0 888 288 -22 454.31237113237376 00:00:00 00:00:00 0.30595939773998737 14.83980914864711 -1.0 -1.0 -1.0 35.0 888 288 -23 188.43753414368308 00:00:00 00:00:00 0.10302957505083073 26.190444971253193 -1.0 -1.0 -1.0 35.0 888 288 -24 432.25074329840373 00:00:00 00:00:00 0.4754213259005369 46.827196897045056 -1.0 -1.0 -1.0 35.0 888 288 -25 579.5866100250605 00:00:00 00:00:00 0.8647609520520425 15.689862449648482 -1.0 -1.0 -1.0 35.0 888 288 -26 5036.7746606005 00:00:00 00:00:00 0.5002904692937721 11.894270840857446 -1.0 -1.0 -1.0 35.0 888 288 -27 143.49252810767797 00:00:00 00:00:00 0.031013550446277915 484.69318911542297 -1.0 -1.0 -1.0 35.0 888 288 -28 896.2325667562126 00:00:00 00:00:00 0.8417691323648457 18.995660966151245 -1.0 -1.0 -1.0 35.0 888 288 -29 695.548826635149 00:00:00 00:00:00 0.6255976121707891 12.325923688964098 -1.0 -1.0 -1.0 35.0 888 288 -30 518.6973214909851 00:00:00 00:00:00 0.23881084420956872 40.622240538994284 -1.0 -1.0 -1.0 35.0 888 288 -31 820.101280852925 00:00:00 00:00:00 1.1612203896837485 11.906421530120177 -1.0 -1.0 -1.0 35.0 888 288 -32 805.6084061258246 00:00:00 00:00:00 0.2630165572065694 11.353905426206259 -1.0 -1.0 -1.0 35.0 888 288 -33 419.29611869030475 00:00:00 00:00:00 0.20647894694486463 16.60160673523299 -1.0 -1.0 -1.0 35.0 888 288 -34 155.6073504242375 00:00:00 00:00:00 0.13973452765175293 66.13285676320632 -1.0 -1.0 -1.0 35.0 888 288 -35 1147.6509354631635 00:00:00 00:00:00 1.0136845781113837 10.989377138257774 -1.0 -1.0 -1.0 35.0 888 288 -36 434.9054218021994 00:00:00 00:00:00 0.392463403996419 238.5088838108578 -1.0 -1.0 -1.0 35.0 888 288 -37 310.16626884466916 00:00:00 00:00:00 0.2231164551134546 14.360447206185484 -1.0 -1.0 -1.0 35.0 888 288 -38 257.9624832084242 00:00:00 00:00:00 0.22600406680326088 21.270338041223077 -1.0 -1.0 -1.0 35.0 888 288 -39 323.316432591907 00:00:00 00:00:00 0.32121684250403904 40.66596088103641 -1.0 -1.0 -1.0 35.0 888 288 -40 387.26342412555556 00:00:00 00:00:00 0.539696339706405 61.00023532034031 -1.0 -1.0 -1.0 35.0 888 288 -41 275.28452072976125 00:00:00 00:00:00 0.13785979810086718 48.83811893549135 -1.0 -1.0 -1.0 35.0 888 288 -42 1067.4327278110532 00:00:00 00:00:00 0.15170970398287423 20.27598540262997 -1.0 -1.0 -1.0 35.0 888 288 -43 730.1731714209269 00:00:00 00:00:00 0.4293026776986884 11.312977703982753 -1.0 -1.0 -1.0 35.0 888 288 -44 879.3689116020875 00:00:00 00:00:00 0.6998025437869662 20.44890692681992 -1.0 -1.0 -1.0 35.0 888 288 -45 140.53262627190654 00:00:00 00:00:00 0.12214775369962899 12.864108327165525 -1.0 -1.0 -1.0 35.0 888 288 -46 138.60386510840408 00:00:00 00:00:00 0.07701889006245974 67.09929347610452 -1.0 -1.0 -1.0 35.0 888 288 -47 457.39367798062347 00:00:00 00:00:00 0.33506149346301406 126.10472883527777 -1.0 -1.0 -1.0 35.0 888 288 -48 720.9874661585104 00:00:00 00:00:00 0.65927892450881 14.19983264302042 -1.0 -1.0 -1.0 35.0 888 288 -49 272.4500208102785 00:00:00 00:00:00 0.10034424225034373 23.153353203332813 -1.0 -1.0 -1.0 35.0 888 288 -50 229.67723852719107 00:00:00 00:00:00 0.1769219063231704 15.939674250127918 -1.0 -1.0 -1.0 35.0 888 288 -51 331.58825171496477 00:00:00 00:00:00 0.28689751062154084 35.76985881730512 -1.0 -1.0 -1.0 35.0 888 288 -52 775.9942188826124 00:00:00 00:00:00 0.9095669398984454 20.787372046573893 -1.0 -1.0 -1.0 35.0 888 288 -53 491.54267256019284 00:00:00 00:00:00 0.40654431593667284 15.873128885187782 -1.0 -1.0 -1.0 35.0 888 288 -54 529.128624543456 00:00:00 00:00:00 0.16591623334154817 30.946910009847507 -1.0 -1.0 -1.0 35.0 888 288 -55 399.1055744918342 00:00:00 00:00:00 0.292741907742198 10.88583325549367 -1.0 -1.0 -1.0 35.0 888 288 -56 431.0128668027263 00:00:00 00:00:00 0.3422864045041924 10.468990242397023 -1.0 -1.0 -1.0 35.0 888 288 -57 190.52132889218325 00:00:00 00:00:00 0.15974983243989516 16.66318259472948 -1.0 -1.0 -1.0 35.0 888 288 -58 113.15491238877307 00:00:00 00:00:00 0.08049647097062486 52.02458333198281 -1.0 -1.0 -1.0 35.0 888 288 -59 559.3752828715894 00:00:00 00:00:00 0.7291607156573523 21.37087749061021 -1.0 -1.0 -1.0 35.0 888 288 -60 1013.2533721372135 00:00:00 00:00:00 0.7908629943907453 14.372771247543605 -1.0 -1.0 -1.0 35.0 888 288 -61 532.6898941820887 00:00:00 00:00:00 0.18519327500192803 29.232208234528766 -1.0 -1.0 -1.0 35.0 888 288 -62 1042.8613965606653 00:00:00 00:00:00 1.156082456130434 12.063938525273805 -1.0 -1.0 -1.0 35.0 888 288 -63 911.7804794768108 00:00:00 00:00:00 1.2696671155527066 14.290569605992246 -1.0 -1.0 -1.0 35.0 888 288 -64 622.5786144978405 00:00:00 00:00:00 0.2321421419770171 86.80356920697237 -1.0 -1.0 -1.0 35.0 888 288 -65 691.7803873564447 00:00:00 00:00:00 0.566082665030762 12.899063417967223 -1.0 -1.0 -1.0 35.0 888 288 -66 425.79471677447805 00:00:00 00:00:00 0.5583782935569813 18.002963736786747 -1.0 -1.0 -1.0 35.0 888 288 -67 897.0848483985724 00:00:00 00:00:00 0.7796414547772865 14.75432572849072 -1.0 -1.0 -1.0 35.0 888 288 -68 414.8587456661204 00:00:00 00:00:00 0.35815179074486864 10.944184015097571 -1.0 -1.0 -1.0 35.0 888 288 -69 663.4430327655946 00:00:00 00:00:00 0.686189269983845 30.236972085102074 -1.0 -1.0 -1.0 35.0 888 288 -70 148.69756109291183 00:00:00 00:00:00 0.08325560189967772 86.90035422142955 -1.0 -1.0 -1.0 35.0 888 288 -71 922.4396243398675 00:00:00 00:00:00 1.1998447582020602 19.921968002308613 -1.0 -1.0 -1.0 35.0 888 288 -72 152.9383064815016 00:00:00 00:00:00 0.09409709853261887 40.184792633110376 -1.0 -1.0 -1.0 35.0 888 288 -73 1294.9170003315799 00:00:00 00:00:00 0.8170199956237274 10.960537313759053 -1.0 -1.0 -1.0 35.0 888 288 -74 197.05046287007687 00:00:00 00:00:00 0.2951445206211712 11.866829353300446 -1.0 -1.0 -1.0 35.0 888 288 -75 1152.0351297397929 00:00:00 00:00:00 1.2414620010612536 22.166897711183683 -1.0 -1.0 -1.0 35.0 888 288 -76 574.7793210141199 00:00:00 00:00:00 0.14034792314624817 19.45858882312784 -1.0 -1.0 -1.0 35.0 888 288 -77 1001.7697964755914 00:00:00 00:00:00 1.0099828152335428 12.261260069068786 -1.0 -1.0 -1.0 35.0 888 288 -78 155.39833051180779 00:00:00 00:00:00 0.14777480836025308 15.413819616047661 -1.0 -1.0 -1.0 35.0 888 288 -79 721.4914716203449 00:00:00 00:00:00 0.2930000060556313 15.099468387889068 -1.0 -1.0 -1.0 35.0 888 288 -80 544.2632638190482 00:00:00 00:00:00 0.22073476719089818 16.937293035041506 -1.0 -1.0 -1.0 35.0 888 288 -81 413.7706136774752 00:00:00 00:00:00 0.42484396192703294 45.53821522890108 -1.0 -1.0 -1.0 35.0 888 288 -82 512.2968332797499 00:00:00 00:00:00 0.1996367236106445 65.41049706254961 -1.0 -1.0 -1.0 35.0 888 288 -83 604.6614773741395 00:00:00 00:00:00 0.5782372859791286 87.47806150668559 -1.0 -1.0 -1.0 35.0 888 288 -84 1518.9511344791895 00:00:00 00:00:00 0.4897463494697832 21.702413338435306 -1.0 -1.0 -1.0 35.0 888 288 -85 643.7854589158669 00:00:00 00:00:00 0.23960444371027312 10.123746839702076 -1.0 -1.0 -1.0 35.0 888 288 -86 126.27385897827892 00:00:00 00:00:00 0.14234129251922795 14.943311830534453 -1.0 -1.0 -1.0 35.0 888 288 -87 1301.6620748304958 00:00:00 00:00:00 1.5359454134547514 12.191300403208118 -1.0 -1.0 -1.0 35.0 888 288 -88 108.5822318161769 00:00:00 00:00:00 0.0627115809935812 98.37197874300217 -1.0 -1.0 -1.0 35.0 888 288 -89 454.4376843789279 00:00:00 00:00:00 0.16412206130274973 11.962983358961118 -1.0 -1.0 -1.0 35.0 888 288 -90 431.94315540593266 00:00:00 00:00:00 0.38273386600196013 46.19921231942614 -1.0 -1.0 -1.0 35.0 888 288 -91 563.7835858233838 00:00:00 00:00:00 0.6068433971386447 12.142815139562412 -1.0 -1.0 -1.0 35.0 888 288 -92 337.3005917385242 00:00:00 00:00:00 0.4972035990778346 14.345395179118563 -1.0 -1.0 -1.0 35.0 888 288 -93 147.4661489599992 00:00:00 00:00:00 0.06008499684256797 504.1131760754106 -1.0 -1.0 -1.0 35.0 888 288 -94 358.8683383314342 00:00:00 00:00:00 0.31826055609871906 18.326073205181974 -1.0 -1.0 -1.0 35.0 888 288 -95 326.5453849076442 00:00:00 00:00:00 0.36836205723592386 12.654073325103571 -1.0 -1.0 -1.0 35.0 888 288 -96 427.76775974005335 00:00:00 00:00:00 0.3607128402843972 25.75055977593358 -1.0 -1.0 -1.0 35.0 888 288 -97 1537.250617119628 00:00:00 00:00:00 0.9227705682639805 16.729141697437374 -1.0 -1.0 -1.0 35.0 888 288 -98 463.1233303504703 00:00:00 00:00:00 0.4693424438885805 11.12631974466657 -1.0 -1.0 -1.0 35.0 888 288 -99 323.04706650622984 00:00:00 00:00:00 0.2394133744966625 44.49101643122364 -1.0 -1.0 -1.0 35.0 888 288 +0 934.0882936484263 00:00:00 00:00:00 0.7921673000689103 29.141753808504433 -1.0 -1.0 -1.0 35.0 888 288 +1 616.5738602258931 00:00:00 00:00:00 0.8905487730463858 10.160405042320036 -1.0 -1.0 -1.0 35.0 888 288 +2 946.6052448796315 00:00:00 00:00:00 0.6740538677349737 10.652772181822607 -1.0 -1.0 -1.0 35.0 888 288 +3 586.5727013781997 00:00:00 00:00:00 0.076308758479808 12.415341138097366 -1.0 -1.0 -1.0 35.0 888 288 +4 2153.3992719321486 00:00:00 00:00:00 0.7719105683125631 12.24289806494758 -1.0 -1.0 -1.0 35.0 888 288 +5 1343.7265012800563 00:00:00 00:00:00 1.6762564040612347 11.09240844582027 -1.0 -1.0 -1.0 35.0 888 288 +6 529.8895060996854 00:00:00 00:00:00 0.6148674711657728 13.133173041616157 -1.0 -1.0 -1.0 35.0 888 288 +7 1022.7800218616562 00:00:00 00:00:00 1.2954271609214607 10.300851732186109 -1.0 -1.0 -1.0 35.0 888 288 +8 412.87460299855934 00:00:00 00:00:00 0.20437591335992997 12.846680716802911 -1.0 -1.0 -1.0 35.0 888 288 +9 701.6217045820609 00:00:00 00:00:00 0.7971964864511405 10.341966201772214 -1.0 -1.0 -1.0 35.0 888 288 +10 205.82201397136845 00:00:00 00:00:00 0.035084600186595774 20.941567140489564 -1.0 -1.0 -1.0 35.0 888 288 +11 1120.7016388974587 00:00:00 00:00:00 1.1190141360593933 11.41994898605589 -1.0 -1.0 -1.0 35.0 888 288 +12 275.5937204880448 00:00:00 00:00:00 0.18195765690908733 91.38695892704916 -1.0 -1.0 -1.0 35.0 888 288 +13 436.349204138444 00:00:00 00:00:00 0.43889114213816116 15.275450861578175 -1.0 -1.0 -1.0 35.0 888 288 +14 295.6406494853049 00:00:00 00:00:00 0.20716845953629162 11.219409175951904 -1.0 -1.0 -1.0 35.0 888 288 +15 291.6471440894715 00:00:00 00:00:00 0.25353674073624666 11.18814447805394 -1.0 -1.0 -1.0 35.0 888 288 +16 1618.8558318442904 00:00:00 00:00:00 0.8938125640216341 24.749670081652773 -1.0 -1.0 -1.0 35.0 888 288 +17 983.9563468218174 00:00:00 00:00:00 0.5198177045244614 12.591584618769005 -1.0 -1.0 -1.0 35.0 888 288 +18 676.3365646959189 00:00:00 00:00:00 0.9106560144996961 10.476494180170306 -1.0 -1.0 -1.0 35.0 888 288 +19 645.2399497353754 00:00:00 00:00:00 0.5654604349809239 10.286082085018421 -1.0 -1.0 -1.0 35.0 888 288 +20 539.5953081699104 00:00:00 00:00:00 0.5580214108898183 35.45318302068964 -1.0 -1.0 -1.0 35.0 888 288 +21 443.6632526190763 00:00:00 00:00:00 0.5313222499329159 14.319328661152863 -1.0 -1.0 -1.0 35.0 888 288 +22 81.45567951589115 00:00:00 00:00:00 0.03213122638515013 15.566080612070119 -1.0 -1.0 -1.0 35.0 888 288 +23 374.91893100970947 00:00:00 00:00:00 0.1090230764363179 29.766613679561118 -1.0 -1.0 -1.0 35.0 888 288 +24 1001.3592175483924 00:00:00 00:00:00 1.1373611970579036 12.965473066837115 -1.0 -1.0 -1.0 35.0 888 288 +25 711.626862396593 00:00:00 00:00:00 0.14735854842537668 13.229440860953233 -1.0 -1.0 -1.0 35.0 888 288 +26 356.4667997984884 00:00:00 00:00:00 0.4130361237696566 20.3929051321851 -1.0 -1.0 -1.0 35.0 888 288 +27 405.6849410353128 00:00:00 00:00:00 0.4022419934538231 12.020783061083032 -1.0 -1.0 -1.0 35.0 888 288 +28 438.56039965043755 00:00:00 00:00:00 0.3146015685795931 14.962222736067005 -1.0 -1.0 -1.0 35.0 888 288 +29 1032.0031340800927 00:00:00 00:00:00 1.029340230030751 12.249965125983609 -1.0 -1.0 -1.0 35.0 888 288 +30 217.45075943955328 00:00:00 00:00:00 0.19288729564479787 13.329278119983234 -1.0 -1.0 -1.0 35.0 888 288 +31 337.87773894962976 00:00:00 00:00:00 0.3614030695297573 15.842464362846469 -1.0 -1.0 -1.0 35.0 888 288 +32 747.9605061576287 00:00:00 00:00:00 0.39117198763502004 11.35266012864496 -1.0 -1.0 -1.0 35.0 888 288 +33 721.5025019044709 00:00:00 00:00:00 0.878613557350197 20.893413876660702 -1.0 -1.0 -1.0 35.0 888 288 +34 1087.4264164816361 00:00:00 00:00:00 1.0208693279100218 12.559979217082482 -1.0 -1.0 -1.0 35.0 888 288 +35 163.23187688161033 00:00:00 00:00:00 0.09607781547873054 78.61411210143966 -1.0 -1.0 -1.0 35.0 888 288 +36 515.446178879044 00:00:00 00:00:00 0.7537064890743636 11.417411530656945 -1.0 -1.0 -1.0 35.0 888 288 +37 392.6137583987218 00:00:00 00:00:00 0.3061610972555812 16.89212239318249 -1.0 -1.0 -1.0 35.0 888 288 +38 719.6387498724689 00:00:00 00:00:00 0.9391830061900761 22.483314792466093 -1.0 -1.0 -1.0 35.0 888 288 +39 1095.9625583277343 00:00:00 00:00:00 1.403367865995526 13.438743382703526 -1.0 -1.0 -1.0 35.0 888 288 +40 758.3297416501492 00:00:00 00:00:00 0.6073490122867862 12.85406115188814 -1.0 -1.0 -1.0 35.0 888 288 +41 196.42239794857238 00:00:00 00:00:00 0.14294900372874897 36.32216074676754 -1.0 -1.0 -1.0 35.0 888 288 +42 300.9638203550653 00:00:00 00:00:00 0.39351448198009875 13.974445331317181 -1.0 -1.0 -1.0 35.0 888 288 +43 819.1568769824953 00:00:00 00:00:00 0.8261842130673787 10.172610101290998 -1.0 -1.0 -1.0 35.0 888 288 +44 672.8493862791563 00:00:00 00:00:00 0.6972176074469371 13.890953706380596 -1.0 -1.0 -1.0 35.0 888 288 +45 171.9322515506212 00:00:00 00:00:00 0.13935267661456355 34.7413812643017 -1.0 -1.0 -1.0 35.0 888 288 +46 1425.9053080008052 00:00:00 00:00:00 1.7059914290120468 12.7456387947933 -1.0 -1.0 -1.0 35.0 888 288 +47 489.25638240698544 00:00:00 00:00:00 0.5445495899312375 16.173416507939116 -1.0 -1.0 -1.0 35.0 888 288 +48 303.6841850164093 00:00:00 00:00:00 0.2974040403504351 10.375992085957462 -1.0 -1.0 -1.0 35.0 888 288 +49 875.3662532129479 00:00:00 00:00:00 0.6489281792071373 10.214362638300399 -1.0 -1.0 -1.0 35.0 888 288 +50 716.6818881006753 00:00:00 00:00:00 0.6543631275867287 14.452157966062835 -1.0 -1.0 -1.0 35.0 888 288 +51 1780.2827105839892 00:00:00 00:00:00 1.3064991817591554 10.3884232109404 -1.0 -1.0 -1.0 35.0 888 288 +52 996.2724896444047 00:00:00 00:00:00 0.5101162519209581 13.213225298728211 -1.0 -1.0 -1.0 35.0 888 288 +53 1698.7961537425167 00:00:00 00:00:00 1.6116303277289115 12.607503410081835 -1.0 -1.0 -1.0 35.0 888 288 +54 1126.5413295499866 00:00:00 00:00:00 1.0047522761163254 17.051568734514 -1.0 -1.0 -1.0 35.0 888 288 +55 450.88973429474333 00:00:00 00:00:00 0.3469329170116606 10.69280211941846 -1.0 -1.0 -1.0 35.0 888 288 +56 973.3753477739596 00:00:00 00:00:00 1.145456105255157 17.506572347460136 -1.0 -1.0 -1.0 35.0 888 288 +57 1489.8174891533963 00:00:00 00:00:00 0.462264715753818 52.449273584894264 -1.0 -1.0 -1.0 35.0 888 288 +58 884.0458853234747 00:00:00 00:00:00 0.9162679525902213 15.638104651466023 -1.0 -1.0 -1.0 35.0 888 288 +59 1371.7618432631962 00:00:00 00:00:00 1.4395856562070026 11.871898609476887 -1.0 -1.0 -1.0 35.0 888 288 +60 779.9123200563506 00:00:00 00:00:00 0.7955326805432289 11.377991388220451 -1.0 -1.0 -1.0 35.0 888 288 +61 244.32835931715942 00:00:00 00:00:00 0.18847008443468843 22.08068896327773 -1.0 -1.0 -1.0 35.0 888 288 +62 170.42643162915402 00:00:00 00:00:00 0.15429769730152024 11.891868553016314 -1.0 -1.0 -1.0 35.0 888 288 +63 697.6397584685641 00:00:00 00:00:00 0.6321468327152712 10.141332615940971 -1.0 -1.0 -1.0 35.0 888 288 +64 896.3411776022986 00:00:00 00:00:00 0.8626798876083768 13.956903508254495 -1.0 -1.0 -1.0 35.0 888 288 +65 673.4550793235062 00:00:00 00:00:00 0.5256410065154973 34.96309565327556 -1.0 -1.0 -1.0 35.0 888 288 +66 409.3504561455062 00:00:00 00:00:00 0.3643567026018635 17.073302923618904 -1.0 -1.0 -1.0 35.0 888 288 +67 1279.5121634320094 00:00:00 00:00:00 1.774116920791602 12.06090823054981 -1.0 -1.0 -1.0 35.0 888 288 +68 455.1932713332237 00:00:00 00:00:00 0.6951890165407291 13.581352455460024 -1.0 -1.0 -1.0 35.0 888 288 +69 212.5080624683044 00:00:00 00:00:00 0.26945046670279904 72.07735525486979 -1.0 -1.0 -1.0 35.0 888 288 +70 208.83082789773542 00:00:00 00:00:00 0.11830405147347496 35.86335125977792 -1.0 -1.0 -1.0 35.0 888 288 +71 218.52612475416436 00:00:00 00:00:00 0.22535984531358233 30.226196813374152 -1.0 -1.0 -1.0 35.0 888 288 +72 1591.9222149862642 00:00:00 00:00:00 1.8110148288203143 12.417298253806685 -1.0 -1.0 -1.0 35.0 888 288 +73 218.79115225062986 00:00:00 00:00:00 0.07542156240114564 14.217908248312149 -1.0 -1.0 -1.0 35.0 888 288 +74 618.4593505087528 00:00:00 00:00:00 0.7604740031756679 11.664308801315117 -1.0 -1.0 -1.0 35.0 888 288 +75 652.0130715223407 00:00:00 00:00:00 0.7035573057923941 12.468451895755134 -1.0 -1.0 -1.0 35.0 888 288 +76 673.0857403351645 00:00:00 00:00:00 0.8590470713360423 14.050855469722556 -1.0 -1.0 -1.0 35.0 888 288 +77 785.3518302003876 00:00:00 00:00:00 1.1165757481045127 11.213356812360049 -1.0 -1.0 -1.0 35.0 888 288 +78 921.812608616067 00:00:00 00:00:00 0.5039827338921795 10.282098860310228 -1.0 -1.0 -1.0 35.0 888 288 +79 656.9337821238685 00:00:00 00:00:00 0.8078859860053357 11.233631650878838 -1.0 -1.0 -1.0 35.0 888 288 +80 1138.704218112882 00:00:00 00:00:00 1.4053123916613104 26.539797617550633 -1.0 -1.0 -1.0 35.0 888 288 +81 620.5653235040173 00:00:00 00:00:00 0.4217561559786075 21.45481401537115 -1.0 -1.0 -1.0 35.0 888 288 +82 838.2486260752073 00:00:00 00:00:00 0.9851844276179345 10.446614306626344 -1.0 -1.0 -1.0 35.0 888 288 +83 552.5443734504142 00:00:00 00:00:00 0.505423229336821 21.543404977178355 -1.0 -1.0 -1.0 35.0 888 288 +84 573.8507253223063 00:00:00 00:00:00 0.4967380348641639 11.084164653890928 -1.0 -1.0 -1.0 35.0 888 288 +85 420.6998183657156 00:00:00 00:00:00 0.45509970537168065 17.32850048985882 -1.0 -1.0 -1.0 35.0 888 288 +86 439.5439082870578 00:00:00 00:00:00 0.24326293577857605 35.833665311539534 -1.0 -1.0 -1.0 35.0 888 288 +87 1130.0482166914742 00:00:00 00:00:00 1.3476543758694524 12.35679852892303 -1.0 -1.0 -1.0 35.0 888 288 +88 450.20877052020955 00:00:00 00:00:00 0.2061046297575687 12.665759219918368 -1.0 -1.0 -1.0 35.0 888 288 +89 110.1657516020332 00:00:00 00:00:00 0.025901855591487423 18.200353247030144 -1.0 -1.0 -1.0 35.0 888 288 +90 443.8604241510417 00:00:00 00:00:00 0.3533866602834305 13.242335799793306 -1.0 -1.0 -1.0 35.0 888 288 +91 444.686556791516 00:00:00 00:00:00 0.4077832962117002 13.80369034602879 -1.0 -1.0 -1.0 35.0 888 288 +92 562.5781184397683 00:00:00 00:00:00 0.6835005902005468 10.27995831481763 -1.0 -1.0 -1.0 35.0 888 288 +93 167.62653574722364 00:00:00 00:00:00 0.02794717342101584 17.570916297751136 -1.0 -1.0 -1.0 35.0 888 288 +94 481.62428400668705 00:00:00 00:00:00 0.4565401559009469 11.826070143713975 -1.0 -1.0 -1.0 35.0 888 288 +95 475.5794761937849 00:00:00 00:00:00 0.5875361292861063 12.987091653999931 -1.0 -1.0 -1.0 35.0 888 288 +96 295.3711932454349 00:00:00 00:00:00 0.24218053028978936 11.871291318754192 -1.0 -1.0 -1.0 35.0 888 288 +97 1364.3326396162597 00:00:00 00:00:00 1.6079574502539073 10.696711455581303 -1.0 -1.0 -1.0 35.0 888 288 +98 584.9391974583868 00:00:00 00:00:00 0.6295707009501357 10.372529937559637 -1.0 -1.0 -1.0 35.0 888 288 +99 1076.886030040255 00:00:00 00:00:00 1.0553245057067546 14.901426606089192 -1.0 -1.0 -1.0 35.0 888 288 diff --git a/papers/lsst/Photometric/create_fake_survey.py b/papers/lsst/Photometric/create_fake_survey.py index 9c48f52a..e66512c0 100644 --- a/papers/lsst/Photometric/create_fake_survey.py +++ b/papers/lsst/Photometric/create_fake_survey.py @@ -24,34 +24,34 @@ def create_fake_survey(smearing=False): sdir = str(resources.files('zdm').joinpath('data/Surveys')) opdir="./" # directory to place fake surveys in. Here! - IntroStr="""# %ECSV 1.0 - # --- - # datatype: - # - {name: TNS, datatype: string} - # - {name: DM, datatype: float64} - # - {name: RA, datatype: string} - # - {name: DEC, datatype: string} - # - {name: Z, datatype: float64} - # - {name: SNR, datatype: float64} - # - {name: WIDTH, datatype: float64} - # - {name: Gl, unit: deg, datatype: float64} - # - {name: Gb, unit: deg, datatype: float64} - # - {name: DMG, datatype: float64} - # - {name: FBAR, datatype: float64} - # - {name: BW, datatype: float64} - # meta: !!omap - # - {survey_data: '{"observing": {"NORM_FRB": 17,"TOBS": 64.68,"MAX_IW": 8, "MAXWMETH": 2}, - # "telescope": {"BEAM": "CRACO_900", "DMMASK": "craco_900_mask.npy", - # "DIAM": 12.0, "NBEAMS": 1, "NBINS": 5, "FBAR": 906, - # "TRES": 13.8, "FRES": 1.0, "THRESH": 1.01}}'}\n""" + Prefix="""# %ECSV 1.0 +# --- +# datatype: +# - {name: TNS, datatype: string} +# - {name: DM, datatype: float64} +# - {name: RA, datatype: string} +# - {name: DEC, datatype: string} +# - {name: Z, datatype: float64} +# - {name: SNR, datatype: float64} +# - {name: WIDTH, datatype: float64} +# - {name: Gl, unit: deg, datatype: float64} +# - {name: Gb, unit: deg, datatype: float64} +# - {name: DMG, datatype: float64} +# - {name: FBAR, datatype: float64} +# - {name: BW, datatype: float64} +# meta: !!omap +# - {survey_data: '{"observing": {"NORM_FRB": 17,"TOBS": 64.68,"MAX_IW": 8, "MAXWMETH": 2""" +# we need to split the obs string into two so we can insert the zfraction as required + Suffix="""}, +# "telescope": {"BEAM": "CRACO_900", "DMMASK": "craco_900_mask.npy", +# "DIAM": 12.0, "NBEAMS": 1, "NBINS": 5, "FBAR": 906, +# "TRES": 13.8, "FRES": 1.0, "THRESH": 1.01}}'}\n""" #param_dict={'sfr_n': 0.21, 'alpha': 0.11, 'lmean': 2.18, 'lsigma': 0.42, 'lEmax': 41.37, # 'lEmin': 39.47, 'gamma': -1.04, 'H0': 70.23, 'halo_method': 0, 'sigmaDMG': 0.0, 'sigmaHalo': 0.0,'lC': -7.61} # use default state state=states.load_state(case="HoffmannHalo25",scat=None,rep=None) - #state.set_astropy_cosmo(Planck18) - #state.update_params(param_dict) name=['CRAFT_CRACO_900'] @@ -62,7 +62,7 @@ def create_fake_survey(smearing=False): samples=gs.GenMCSample(100) zvals=np.zeros(len(samples)) fp=open(opdir+"Spectroscopic.ecsv","w+") - fp.write(IntroStr) + fp.write(Prefix+Suffix) fp.write("TNS DM RA DEC Z SNR WIDTH Gl Gb DMG FBAR BW\n") for i in range(len(samples)): fp.write('{0:5}'.format(str(i))) @@ -89,7 +89,7 @@ def create_fake_survey(smearing=False): zvals[i]=samples[i][0] fp=open(opdir+"Smeared.ecsv","w+") - fp.write(IntroStr) + fp.write(Prefix+', "Z_PHOTO": 0.03'+Suffix) fp.write("TNS DM RA DEC Z SNR WIDTH Gl Gb DMG FBAR BW\n") smear_error=random.normal(loc=0,scale=sigma,size=100) newvals=zvals+smear_error @@ -115,8 +115,10 @@ def create_fake_survey(smearing=False): fp=open(opdir+"zFrac.ecsv","w+") fp1=open(opdir+"Smeared_and_zFrac.ecsv","w+") - fp.write(IntroStr) - fp1.write(IntroStr) + + fp.write(Prefix+', "Z_FRACTION": 24.7'+Suffix) + fp1.write(Prefix+', "Z_FRACTION": 24.7, "Z_PHOTO": 0.03'+Suffix) + fp.write("TNS DM RA DEC Z SNR WIDTH Gl Gb DMG FBAR BW\n") fp1.write("TNS DM RA DEC Z SNR WIDTH Gl Gb DMG FBAR BW\n") for i in range(len(samples)): diff --git a/papers/lsst/Photometric/plot_2dgrids.py b/papers/lsst/Photometric/plot_2dgrids.py new file mode 100644 index 00000000..79426ab7 --- /dev/null +++ b/papers/lsst/Photometric/plot_2dgrids.py @@ -0,0 +1,84 @@ +""" +Evaluates the likelihood of a slice through H0 for various surveys +""" + +import argparse +import numpy as np +import os + +from zdm import figures +from zdm import iteration as it +from zdm import loading +from zdm import parameters +from zdm import repeat_grid as zdm_repeat_grid +from zdm import MCMC +from zdm import survey +from zdm import states +from astropy.cosmology import Planck18 +import importlib.resources as resources +from numpy import random +import matplotlib.pyplot as plt +import time + +def main(): + """ + Plots 2D zDM grids + """ + # Set state + state=states.load_state(case="HoffmannHalo25",scat=None,rep=None) + sdir = resources.files('zdm').joinpath('../papers/lsst/Photometric') + names = ["Spectroscopic","Smeared","zFrac","Smeared_and_zFrac"] + + ss,gs = loading.surveys_and_grids( + survey_names=names,repeaters=False,init_state=state,sdir=sdir) + plot_grids(gs,ss,"./") + + +#============================================================================== +""" +Function: plot_grids +Date: 10/01/2024 +Purpose: + Plot grids. Adapted from zdm/scripts/plot_pzdm_grid.py + +Imports: + grids = list of grids + surveys = list of surveys + outdir = output directory + val = parameter value for this grid +""" +def plot_grids(grids, surveys, outdir): + for g,s in zip(grids, surveys): + zvals=[] + dmvals=[] + nozlist=[] + + if s.zlist is not None: + for iFRB in s.zlist: + zvals.append(s.Zs[iFRB]) + dmvals.append(s.DMEGs[iFRB]) + if s.nozlist is not None: + for dm in s.DMEGs[s.nozlist]: + nozlist.append(dm) + + frbzvals = np.array(zvals) + frbdmvals = np.array(dmvals) + + figures.plot_grid( + g.rates, + g.zvals, + g.dmvals, + name=outdir + s.name + "_zDM.png", + norm=3, + log=True, + label="$\\log_{10} p({\\rm DM}_{\\rm EG},z)$ [a.u.]", + project=False, + FRBDMs=frbdmvals, + FRBZs=frbzvals, + Aconts=[0.01, 0.1, 0.5], + zmax=3.0, + DMmax=3000, + # DMlines=nozlist, + ) + +main() diff --git a/papers/lsst/Photometric/plot_H0_slice.py b/papers/lsst/Photometric/plot_H0_slice.py new file mode 100644 index 00000000..6ec52b0a --- /dev/null +++ b/papers/lsst/Photometric/plot_H0_slice.py @@ -0,0 +1,103 @@ +""" +Generates a plot of previously calculated slice through H0 +""" + +import argparse +import numpy as np +import os + +from zdm import figures +from zdm import iteration as it + +from zdm import parameters +from zdm import repeat_grid as zdm_repeat_grid +from zdm import MCMC +from zdm import survey +from zdm import states +from astropy.cosmology import Planck18 + +from numpy import random +import matplotlib.pyplot as plt +import time + +import matplotlib +defaultsize=18 +ds=4 +font = {'family' : 'Helvetica', + 'weight' : 'normal', + 'size' : defaultsize} +matplotlib.rc('font', **font) + +def main(): + """ + Main routine to create plots and extract characteristic parameters + """ + ll_lists=np.load("ll_lists.npy") + vals = np.load("h0vals.npy") + + nh,ns = ll_lists.shape + + plt.figure() + linestyles=["-","--",":","-."] + # up to the user to get thius order right! Use e.g. + # python run_H0_slice.py -n 10 --min=50 --max=100 -f Spectroscopic Smeared zFrac Smeared_and_zFrac + + s_names=["All hosts, spec-zs","$\\sigma_z=0.03$","$m_r^{\\rm lim}=24.7$","$\\sigma_z=0.03,~m_r^{\\rm lim}=24.7$"] + plt.clf() + llsum = np.zeros(ll_lists.shape[0]) + FWHM=[] + for i in np.arange(ns): + + lls = ll_lists[:, i] + + lls[lls < -1e10] = -np.inf + lls[np.argwhere(np.isnan(lls))] = -np.inf + + llsum += lls + + lls = lls - np.max(lls) + lls=10**lls + index1=np.where(lls>=0.5)[0][0] + index2=np.where(lls>=0.5)[0][-1] + root1=vals[index1-1]-(0.5-lls[index1-1])*(vals[index1]-vals[index1-1])/(lls[index1]-lls[index1-1]) + root2=vals[index2]-(0.5-lls[index2])*(vals[index2+1]-vals[index2])/(lls[index2+1]-lls[index2]) + FWHM.append(root2-root1) + # plt.figure() + # plt.clf() + plt.plot(vals, lls, label=s_names[i],ls=linestyles[i]) + plt.xlabel('$H_0$ (km s$^{-1}$ Mpc$^{-1}$)',fontsize=14) + plt.ylabel('$\\frac{\\mathcal{L}}{max(\\mathcal{L})}$',fontsize=18) + # plt.savefig(os.path.join(outdir, s.name + ".pdf")) + #print("Max H0:",vals[np.where(lls==1.0)[0][0]]) + + plt.minorticks_on() + plt.tick_params(axis='y', which='major', labelsize=14) # To set tick label fontsize + plt.tick_params(axis='y', which='major', length=9) # To set tick size + plt.tick_params(axis='y', which='minor', length=4.5) # To set tick size + plt.tick_params(axis='y', which='both',direction='in',right='on', top='on') + + plt.tick_params(axis='x', which='major', labelsize=14) # To set tick label fontsize + plt.tick_params(axis='x', which='major', length=9) # To set tick size + plt.tick_params(axis='x', which='minor', length=4.5) # To set tick size + plt.tick_params(axis='x', which='both',direction='in',right='on', top='on') + + #peak=vals[np.argwhere(llsum == np.max(llsum))[0]] + plt.xlim(60,90) + plt.ylim(0,1) + + + plt.plot([70.63,70.63],[0,1],color="black",linestyle=":") + plt.text(69,0.06,"$H_0^{\\rm sim}$",rotation=90,fontsize=14) + + #plt.axvline(peak,ls='--') + #plt.legend(loc='upper left') + plt.legend(fontsize=10) + plt.tight_layout() + plt.savefig("H0_scan_linear.png") + percentage=(FWHM/FWHM[0]-1)*100 + for i,name in enumerate(s_names): + print(name," FWHM is ",FWHM[i]," frac is ",percentage[i]) + #print("FWHM:Spectroscopic,Photometric,zFrac,Photometric+zfrac\n",FWHM,percentage) + + +main() diff --git a/papers/lsst/Photometric/run_H0_slice.py b/papers/lsst/Photometric/run_H0_slice.py new file mode 100644 index 00000000..319a73ed --- /dev/null +++ b/papers/lsst/Photometric/run_H0_slice.py @@ -0,0 +1,100 @@ +""" +Evaluates the likelihood of a slice through H0 for various surveys +""" + +import argparse +import numpy as np +import os + +from zdm import figures +from zdm import iteration as it + +from zdm import parameters +from zdm import repeat_grid as zdm_repeat_grid +from zdm import MCMC +from zdm import survey +from zdm import states +from astropy.cosmology import Planck18 + +from numpy import random +import matplotlib.pyplot as plt +import time + +def main(): + """ + run with: + python run_H0_slice.py -n 10 --min=50 --max=100 -f Smeared zFrac Spectroscopic Smeared_and_zFrac + + """ + t0 = time.time() + parser = argparse.ArgumentParser() + #parser.add_argument(dest='param',type=str,help="Parameter to do the slice in") + parser.add_argument('--min',type=float,help="Min value") + parser.add_argument('--max',type=float,help="Max value") + parser.add_argument('-f', '--files', default=None, nargs='+', type=str, help="Survey file names") + parser.add_argument('-n',dest='n',type=int,default=50,help="Number of values") + # parser.add_argument('-r',dest='repeaters',default=False,action='store_true',help="Surveys are repeater surveys") + args = parser.parse_args() + + vals = np.linspace(args.min, args.max, args.n) + + # Set state + state=states.load_state(case="HoffmannHalo25",scat=None,rep=None) + + grid_params = {} + grid_params['dmmax'] = 7000.0 + grid_params['ndm'] = 1400 + grid_params['nz'] = 500 + ddm = grid_params['dmmax'] / grid_params['ndm'] + dmvals = (np.arange(grid_params['ndm']) + 1) * ddm + + # Initialise surveys + surveys = [] + if args.files is not None: + for survey_name in args.files: + print("Loading survey ",survey_name) + s = survey.load_survey(survey_name, state, dmvals,sdir="./") + surveys.append(s) + + + # state.update_param('halo_method', 1) + # state.update_param(args.param, vals[0]) + + outdir = 'cube/' + 'H0' + '/' + if not os.path.exists(outdir): + os.makedirs(outdir) + + ll_lists = [] + for val in vals: + print("val:", val) + param = {"H0": {'min': -np.inf, 'max': np.inf}} + ll=0 + ll_list=[] + sll, sll_list = MCMC.calc_log_posterior([val], state, param,[surveys,[]], grid_params, ind_surveys=True)#,psnr=True) + + ll_lists.append(sll_list) + + + ll_lists = np.asarray(ll_lists) + np.save("ll_lists.npy",ll_lists) + np.save("h0vals.npy",vals) + +#============================================================================== +""" +Function: commasep +Date: 23/08/2022 +Purpose: + Turn a string of variables seperated by commas into a list + +Imports: + s = String of variables + +Exports: + List conversion of s +""" +def commasep(s): + return list(map(str, s.split(','))) + +#============================================================================== + +main() diff --git a/zdm/scripts/H0_scan/run_slice.py b/papers/lsst/Photometric/run_slice.py similarity index 100% rename from zdm/scripts/H0_scan/run_slice.py rename to papers/lsst/Photometric/run_slice.py diff --git a/papers/lsst/Photometric/zFrac.ecsv b/papers/lsst/Photometric/zFrac.ecsv index 3b5c78b0..920edc9c 100644 --- a/papers/lsst/Photometric/zFrac.ecsv +++ b/papers/lsst/Photometric/zFrac.ecsv @@ -14,93 +14,82 @@ # - {name: FBAR, datatype: float64} # - {name: BW, datatype: float64} # meta: !!omap -# - {survey_data: '{"observing": {"NORM_FRB": 17,"TOBS": 64.68,"MAX_IW": 8, "MAXWMETH": 2}, +# - {survey_data: '{"observing": {"NORM_FRB": 17,"TOBS": 64.68,"MAX_IW": 8, "MAXWMETH": 2, "Z_FRACTION": 24.7}, # "telescope": {"BEAM": "CRACO_900", "DMMASK": "craco_900_mask.npy", # "DIAM": 12.0, "NBEAMS": 1, "NBINS": 5, "FBAR": 906, # "TRES": 13.8, "FRES": 1.0, "THRESH": 1.01}}'} TNS DM RA DEC Z SNR WIDTH Gl Gb DMG FBAR BW -1 1293.8332446347993 00:00:00 00:00:00 0.8413293511214376 12.346871755650161 -1.0 -1.0 -1.0 35.0 888 288 -5 789.2339950025221 00:00:00 00:00:00 0.25506480932174347 15.974081920078653 -1.0 -1.0 -1.0 35.0 888 288 -6 322.2960237321481 00:00:00 00:00:00 0.27128843344528 41.61528388827413 -1.0 -1.0 -1.0 35.0 888 288 -7 291.26523564494755 00:00:00 00:00:00 0.4466332273656116 35.39286498287345 -1.0 -1.0 -1.0 35.0 888 288 -8 265.6172817387779 00:00:00 00:00:00 0.18436196594727836 17.005499594805812 -1.0 -1.0 -1.0 35.0 888 288 -9 837.3735197561044 00:00:00 00:00:00 0.6950751437047783 22.368861955427324 -1.0 -1.0 -1.0 35.0 888 288 -10 430.42288234797786 00:00:00 00:00:00 0.556902505194651 31.14684691070062 -1.0 -1.0 -1.0 35.0 888 288 -11 338.8720812210228 00:00:00 00:00:00 0.4188292757070367 11.514360186435827 -1.0 -1.0 -1.0 35.0 888 288 -12 459.8929794701774 00:00:00 00:00:00 0.5903928193481528 28.970291709672008 -1.0 -1.0 -1.0 35.0 888 288 -13 514.2392669873378 00:00:00 00:00:00 0.4823674377490287 26.23234752429593 -1.0 -1.0 -1.0 35.0 888 288 -14 444.82316308571853 00:00:00 00:00:00 0.18093771758703725 10.224780135231132 -1.0 -1.0 -1.0 35.0 888 288 -15 423.4287617724789 00:00:00 00:00:00 0.1281487760475812 16.93166467819824 -1.0 -1.0 -1.0 35.0 888 288 -16 250.18714908014232 00:00:00 00:00:00 0.14584265666328355 16.825688142584564 -1.0 -1.0 -1.0 35.0 888 288 -17 230.04604721703424 00:00:00 00:00:00 0.22897196618507287 30.06214700360865 -1.0 -1.0 -1.0 35.0 888 288 -18 620.1606829061071 00:00:00 00:00:00 0.32878933144592676 48.42640123698046 -1.0 -1.0 -1.0 35.0 888 288 -19 548.9677620404942 00:00:00 00:00:00 0.205349720070631 14.25071741051486 -1.0 -1.0 -1.0 35.0 888 288 -20 208.45270441661265 00:00:00 00:00:00 0.15446192559965205 11.795177754311347 -1.0 -1.0 -1.0 35.0 888 288 -22 454.31237113237376 00:00:00 00:00:00 0.30595939773998737 14.83980914864711 -1.0 -1.0 -1.0 35.0 888 288 -23 188.43753414368308 00:00:00 00:00:00 0.10302957505083073 26.190444971253193 -1.0 -1.0 -1.0 35.0 888 288 -24 432.25074329840373 00:00:00 00:00:00 0.4754213259005369 46.827196897045056 -1.0 -1.0 -1.0 35.0 888 288 -25 579.5866100250605 00:00:00 00:00:00 0.8647609520520425 15.689862449648482 -1.0 -1.0 -1.0 35.0 888 288 -26 5036.7746606005 00:00:00 00:00:00 0.5002904692937721 11.894270840857446 -1.0 -1.0 -1.0 35.0 888 288 -27 143.49252810767797 00:00:00 00:00:00 0.031013550446277915 484.69318911542297 -1.0 -1.0 -1.0 35.0 888 288 -29 695.548826635149 00:00:00 00:00:00 0.6255976121707891 12.325923688964098 -1.0 -1.0 -1.0 35.0 888 288 -30 518.6973214909851 00:00:00 00:00:00 0.23881084420956872 40.622240538994284 -1.0 -1.0 -1.0 35.0 888 288 -31 820.101280852925 00:00:00 00:00:00 1.1612203896837485 11.906421530120177 -1.0 -1.0 -1.0 35.0 888 288 -32 805.6084061258246 00:00:00 00:00:00 0.2630165572065694 11.353905426206259 -1.0 -1.0 -1.0 35.0 888 288 -33 419.29611869030475 00:00:00 00:00:00 0.20647894694486463 16.60160673523299 -1.0 -1.0 -1.0 35.0 888 288 -34 155.6073504242375 00:00:00 00:00:00 0.13973452765175293 66.13285676320632 -1.0 -1.0 -1.0 35.0 888 288 -35 1147.6509354631635 00:00:00 00:00:00 1.0136845781113837 10.989377138257774 -1.0 -1.0 -1.0 35.0 888 288 -36 434.9054218021994 00:00:00 00:00:00 0.392463403996419 238.5088838108578 -1.0 -1.0 -1.0 35.0 888 288 -37 310.16626884466916 00:00:00 00:00:00 0.2231164551134546 14.360447206185484 -1.0 -1.0 -1.0 35.0 888 288 -38 257.9624832084242 00:00:00 00:00:00 0.22600406680326088 21.270338041223077 -1.0 -1.0 -1.0 35.0 888 288 -40 387.26342412555556 00:00:00 00:00:00 0.539696339706405 61.00023532034031 -1.0 -1.0 -1.0 35.0 888 288 -41 275.28452072976125 00:00:00 00:00:00 0.13785979810086718 48.83811893549135 -1.0 -1.0 -1.0 35.0 888 288 -42 1067.4327278110532 00:00:00 00:00:00 0.15170970398287423 20.27598540262997 -1.0 -1.0 -1.0 35.0 888 288 -43 730.1731714209269 00:00:00 00:00:00 0.4293026776986884 11.312977703982753 -1.0 -1.0 -1.0 35.0 888 288 -44 879.3689116020875 00:00:00 00:00:00 0.6998025437869662 20.44890692681992 -1.0 -1.0 -1.0 35.0 888 288 -45 140.53262627190654 00:00:00 00:00:00 0.12214775369962899 12.864108327165525 -1.0 -1.0 -1.0 35.0 888 288 -46 138.60386510840408 00:00:00 00:00:00 0.07701889006245974 67.09929347610452 -1.0 -1.0 -1.0 35.0 888 288 -47 457.39367798062347 00:00:00 00:00:00 0.33506149346301406 126.10472883527777 -1.0 -1.0 -1.0 35.0 888 288 -48 720.9874661585104 00:00:00 00:00:00 0.65927892450881 14.19983264302042 -1.0 -1.0 -1.0 35.0 888 288 -49 272.4500208102785 00:00:00 00:00:00 0.10034424225034373 23.153353203332813 -1.0 -1.0 -1.0 35.0 888 288 -50 229.67723852719107 00:00:00 00:00:00 0.1769219063231704 15.939674250127918 -1.0 -1.0 -1.0 35.0 888 288 -51 331.58825171496477 00:00:00 00:00:00 0.28689751062154084 35.76985881730512 -1.0 -1.0 -1.0 35.0 888 288 -53 491.54267256019284 00:00:00 00:00:00 0.40654431593667284 15.873128885187782 -1.0 -1.0 -1.0 35.0 888 288 -54 529.128624543456 00:00:00 00:00:00 0.16591623334154817 30.946910009847507 -1.0 -1.0 -1.0 35.0 888 288 -55 399.1055744918342 00:00:00 00:00:00 0.292741907742198 10.88583325549367 -1.0 -1.0 -1.0 35.0 888 288 -56 431.0128668027263 00:00:00 00:00:00 0.3422864045041924 10.468990242397023 -1.0 -1.0 -1.0 35.0 888 288 -57 190.52132889218325 00:00:00 00:00:00 0.15974983243989516 16.66318259472948 -1.0 -1.0 -1.0 35.0 888 288 -58 113.15491238877307 00:00:00 00:00:00 0.08049647097062486 52.02458333198281 -1.0 -1.0 -1.0 35.0 888 288 -59 559.3752828715894 00:00:00 00:00:00 0.7291607156573523 21.37087749061021 -1.0 -1.0 -1.0 35.0 888 288 -61 532.6898941820887 00:00:00 00:00:00 0.18519327500192803 29.232208234528766 -1.0 -1.0 -1.0 35.0 888 288 -64 622.5786144978405 00:00:00 00:00:00 0.2321421419770171 86.80356920697237 -1.0 -1.0 -1.0 35.0 888 288 -65 691.7803873564447 00:00:00 00:00:00 0.566082665030762 12.899063417967223 -1.0 -1.0 -1.0 35.0 888 288 -66 425.79471677447805 00:00:00 00:00:00 0.5583782935569813 18.002963736786747 -1.0 -1.0 -1.0 35.0 888 288 -68 414.8587456661204 00:00:00 00:00:00 0.35815179074486864 10.944184015097571 -1.0 -1.0 -1.0 35.0 888 288 -69 663.4430327655946 00:00:00 00:00:00 0.686189269983845 30.236972085102074 -1.0 -1.0 -1.0 35.0 888 288 -70 148.69756109291183 00:00:00 00:00:00 0.08325560189967772 86.90035422142955 -1.0 -1.0 -1.0 35.0 888 288 -72 152.9383064815016 00:00:00 00:00:00 0.09409709853261887 40.184792633110376 -1.0 -1.0 -1.0 35.0 888 288 -74 197.05046287007687 00:00:00 00:00:00 0.2951445206211712 11.866829353300446 -1.0 -1.0 -1.0 35.0 888 288 -75 1152.0351297397929 00:00:00 00:00:00 1.2414620010612536 22.166897711183683 -1.0 -1.0 -1.0 35.0 888 288 -76 574.7793210141199 00:00:00 00:00:00 0.14034792314624817 19.45858882312784 -1.0 -1.0 -1.0 35.0 888 288 -77 1001.7697964755914 00:00:00 00:00:00 1.0099828152335428 12.261260069068786 -1.0 -1.0 -1.0 35.0 888 288 -78 155.39833051180779 00:00:00 00:00:00 0.14777480836025308 15.413819616047661 -1.0 -1.0 -1.0 35.0 888 288 -79 721.4914716203449 00:00:00 00:00:00 0.2930000060556313 15.099468387889068 -1.0 -1.0 -1.0 35.0 888 288 -80 544.2632638190482 00:00:00 00:00:00 0.22073476719089818 16.937293035041506 -1.0 -1.0 -1.0 35.0 888 288 -81 413.7706136774752 00:00:00 00:00:00 0.42484396192703294 45.53821522890108 -1.0 -1.0 -1.0 35.0 888 288 -82 512.2968332797499 00:00:00 00:00:00 0.1996367236106445 65.41049706254961 -1.0 -1.0 -1.0 35.0 888 288 -83 604.6614773741395 00:00:00 00:00:00 0.5782372859791286 87.47806150668559 -1.0 -1.0 -1.0 35.0 888 288 -84 1518.9511344791895 00:00:00 00:00:00 0.4897463494697832 21.702413338435306 -1.0 -1.0 -1.0 35.0 888 288 -85 643.7854589158669 00:00:00 00:00:00 0.23960444371027312 10.123746839702076 -1.0 -1.0 -1.0 35.0 888 288 -86 126.27385897827892 00:00:00 00:00:00 0.14234129251922795 14.943311830534453 -1.0 -1.0 -1.0 35.0 888 288 -88 108.5822318161769 00:00:00 00:00:00 0.0627115809935812 98.37197874300217 -1.0 -1.0 -1.0 35.0 888 288 -89 454.4376843789279 00:00:00 00:00:00 0.16412206130274973 11.962983358961118 -1.0 -1.0 -1.0 35.0 888 288 -90 431.94315540593266 00:00:00 00:00:00 0.38273386600196013 46.19921231942614 -1.0 -1.0 -1.0 35.0 888 288 -91 563.7835858233838 00:00:00 00:00:00 0.6068433971386447 12.142815139562412 -1.0 -1.0 -1.0 35.0 888 288 -92 337.3005917385242 00:00:00 00:00:00 0.4972035990778346 14.345395179118563 -1.0 -1.0 -1.0 35.0 888 288 -93 147.4661489599992 00:00:00 00:00:00 0.06008499684256797 504.1131760754106 -1.0 -1.0 -1.0 35.0 888 288 -94 358.8683383314342 00:00:00 00:00:00 0.31826055609871906 18.326073205181974 -1.0 -1.0 -1.0 35.0 888 288 -95 326.5453849076442 00:00:00 00:00:00 0.36836205723592386 12.654073325103571 -1.0 -1.0 -1.0 35.0 888 288 -96 427.76775974005335 00:00:00 00:00:00 0.3607128402843972 25.75055977593358 -1.0 -1.0 -1.0 35.0 888 288 -97 1537.250617119628 00:00:00 00:00:00 0.9227705682639805 16.729141697437374 -1.0 -1.0 -1.0 35.0 888 288 -98 463.1233303504703 00:00:00 00:00:00 0.4693424438885805 11.12631974466657 -1.0 -1.0 -1.0 35.0 888 288 -99 323.04706650622984 00:00:00 00:00:00 0.2394133744966625 44.49101643122364 -1.0 -1.0 -1.0 35.0 888 288 +1 616.5738602258931 00:00:00 00:00:00 0.8905487730463858 10.160405042320036 -1.0 -1.0 -1.0 35.0 888 288 +2 946.6052448796315 00:00:00 00:00:00 0.6740538677349737 10.652772181822607 -1.0 -1.0 -1.0 35.0 888 288 +3 586.5727013781997 00:00:00 00:00:00 0.076308758479808 12.415341138097366 -1.0 -1.0 -1.0 35.0 888 288 +4 2153.3992719321486 00:00:00 00:00:00 0.7719105683125631 12.24289806494758 -1.0 -1.0 -1.0 35.0 888 288 +6 529.8895060996854 00:00:00 00:00:00 0.6148674711657728 13.133173041616157 -1.0 -1.0 -1.0 35.0 888 288 +8 412.87460299855934 00:00:00 00:00:00 0.20437591335992997 12.846680716802911 -1.0 -1.0 -1.0 35.0 888 288 +9 701.6217045820609 00:00:00 00:00:00 0.7971964864511405 10.341966201772214 -1.0 -1.0 -1.0 35.0 888 288 +10 205.82201397136845 00:00:00 00:00:00 0.035084600186595774 20.941567140489564 -1.0 -1.0 -1.0 35.0 888 288 +12 275.5937204880448 00:00:00 00:00:00 0.18195765690908733 91.38695892704916 -1.0 -1.0 -1.0 35.0 888 288 +13 436.349204138444 00:00:00 00:00:00 0.43889114213816116 15.275450861578175 -1.0 -1.0 -1.0 35.0 888 288 +14 295.6406494853049 00:00:00 00:00:00 0.20716845953629162 11.219409175951904 -1.0 -1.0 -1.0 35.0 888 288 +15 291.6471440894715 00:00:00 00:00:00 0.25353674073624666 11.18814447805394 -1.0 -1.0 -1.0 35.0 888 288 +16 1618.8558318442904 00:00:00 00:00:00 0.8938125640216341 24.749670081652773 -1.0 -1.0 -1.0 35.0 888 288 +17 983.9563468218174 00:00:00 00:00:00 0.5198177045244614 12.591584618769005 -1.0 -1.0 -1.0 35.0 888 288 +19 645.2399497353754 00:00:00 00:00:00 0.5654604349809239 10.286082085018421 -1.0 -1.0 -1.0 35.0 888 288 +20 539.5953081699104 00:00:00 00:00:00 0.5580214108898183 35.45318302068964 -1.0 -1.0 -1.0 35.0 888 288 +21 443.6632526190763 00:00:00 00:00:00 0.5313222499329159 14.319328661152863 -1.0 -1.0 -1.0 35.0 888 288 +22 81.45567951589115 00:00:00 00:00:00 0.03213122638515013 15.566080612070119 -1.0 -1.0 -1.0 35.0 888 288 +23 374.91893100970947 00:00:00 00:00:00 0.1090230764363179 29.766613679561118 -1.0 -1.0 -1.0 35.0 888 288 +25 711.626862396593 00:00:00 00:00:00 0.14735854842537668 13.229440860953233 -1.0 -1.0 -1.0 35.0 888 288 +26 356.4667997984884 00:00:00 00:00:00 0.4130361237696566 20.3929051321851 -1.0 -1.0 -1.0 35.0 888 288 +27 405.6849410353128 00:00:00 00:00:00 0.4022419934538231 12.020783061083032 -1.0 -1.0 -1.0 35.0 888 288 +28 438.56039965043755 00:00:00 00:00:00 0.3146015685795931 14.962222736067005 -1.0 -1.0 -1.0 35.0 888 288 +29 1032.0031340800927 00:00:00 00:00:00 1.029340230030751 12.249965125983609 -1.0 -1.0 -1.0 35.0 888 288 +30 217.45075943955328 00:00:00 00:00:00 0.19288729564479787 13.329278119983234 -1.0 -1.0 -1.0 35.0 888 288 +31 337.87773894962976 00:00:00 00:00:00 0.3614030695297573 15.842464362846469 -1.0 -1.0 -1.0 35.0 888 288 +32 747.9605061576287 00:00:00 00:00:00 0.39117198763502004 11.35266012864496 -1.0 -1.0 -1.0 35.0 888 288 +35 163.23187688161033 00:00:00 00:00:00 0.09607781547873054 78.61411210143966 -1.0 -1.0 -1.0 35.0 888 288 +37 392.6137583987218 00:00:00 00:00:00 0.3061610972555812 16.89212239318249 -1.0 -1.0 -1.0 35.0 888 288 +40 758.3297416501492 00:00:00 00:00:00 0.6073490122867862 12.85406115188814 -1.0 -1.0 -1.0 35.0 888 288 +41 196.42239794857238 00:00:00 00:00:00 0.14294900372874897 36.32216074676754 -1.0 -1.0 -1.0 35.0 888 288 +42 300.9638203550653 00:00:00 00:00:00 0.39351448198009875 13.974445331317181 -1.0 -1.0 -1.0 35.0 888 288 +43 819.1568769824953 00:00:00 00:00:00 0.8261842130673787 10.172610101290998 -1.0 -1.0 -1.0 35.0 888 288 +45 171.9322515506212 00:00:00 00:00:00 0.13935267661456355 34.7413812643017 -1.0 -1.0 -1.0 35.0 888 288 +47 489.25638240698544 00:00:00 00:00:00 0.5445495899312375 16.173416507939116 -1.0 -1.0 -1.0 35.0 888 288 +48 303.6841850164093 00:00:00 00:00:00 0.2974040403504351 10.375992085957462 -1.0 -1.0 -1.0 35.0 888 288 +49 875.3662532129479 00:00:00 00:00:00 0.6489281792071373 10.214362638300399 -1.0 -1.0 -1.0 35.0 888 288 +50 716.6818881006753 00:00:00 00:00:00 0.6543631275867287 14.452157966062835 -1.0 -1.0 -1.0 35.0 888 288 +51 1780.2827105839892 00:00:00 00:00:00 1.3064991817591554 10.3884232109404 -1.0 -1.0 -1.0 35.0 888 288 +52 996.2724896444047 00:00:00 00:00:00 0.5101162519209581 13.213225298728211 -1.0 -1.0 -1.0 35.0 888 288 +54 1126.5413295499866 00:00:00 00:00:00 1.0047522761163254 17.051568734514 -1.0 -1.0 -1.0 35.0 888 288 +55 450.88973429474333 00:00:00 00:00:00 0.3469329170116606 10.69280211941846 -1.0 -1.0 -1.0 35.0 888 288 +57 1489.8174891533963 00:00:00 00:00:00 0.462264715753818 52.449273584894264 -1.0 -1.0 -1.0 35.0 888 288 +61 244.32835931715942 00:00:00 00:00:00 0.18847008443468843 22.08068896327773 -1.0 -1.0 -1.0 35.0 888 288 +62 170.42643162915402 00:00:00 00:00:00 0.15429769730152024 11.891868553016314 -1.0 -1.0 -1.0 35.0 888 288 +64 896.3411776022986 00:00:00 00:00:00 0.8626798876083768 13.956903508254495 -1.0 -1.0 -1.0 35.0 888 288 +65 673.4550793235062 00:00:00 00:00:00 0.5256410065154973 34.96309565327556 -1.0 -1.0 -1.0 35.0 888 288 +66 409.3504561455062 00:00:00 00:00:00 0.3643567026018635 17.073302923618904 -1.0 -1.0 -1.0 35.0 888 288 +68 455.1932713332237 00:00:00 00:00:00 0.6951890165407291 13.581352455460024 -1.0 -1.0 -1.0 35.0 888 288 +69 212.5080624683044 00:00:00 00:00:00 0.26945046670279904 72.07735525486979 -1.0 -1.0 -1.0 35.0 888 288 +70 208.83082789773542 00:00:00 00:00:00 0.11830405147347496 35.86335125977792 -1.0 -1.0 -1.0 35.0 888 288 +71 218.52612475416436 00:00:00 00:00:00 0.22535984531358233 30.226196813374152 -1.0 -1.0 -1.0 35.0 888 288 +73 218.79115225062986 00:00:00 00:00:00 0.07542156240114564 14.217908248312149 -1.0 -1.0 -1.0 35.0 888 288 +74 618.4593505087528 00:00:00 00:00:00 0.7604740031756679 11.664308801315117 -1.0 -1.0 -1.0 35.0 888 288 +75 652.0130715223407 00:00:00 00:00:00 0.7035573057923941 12.468451895755134 -1.0 -1.0 -1.0 35.0 888 288 +76 673.0857403351645 00:00:00 00:00:00 0.8590470713360423 14.050855469722556 -1.0 -1.0 -1.0 35.0 888 288 +79 656.9337821238685 00:00:00 00:00:00 0.8078859860053357 11.233631650878838 -1.0 -1.0 -1.0 35.0 888 288 +80 1138.704218112882 00:00:00 00:00:00 1.4053123916613104 26.539797617550633 -1.0 -1.0 -1.0 35.0 888 288 +81 620.5653235040173 00:00:00 00:00:00 0.4217561559786075 21.45481401537115 -1.0 -1.0 -1.0 35.0 888 288 +83 552.5443734504142 00:00:00 00:00:00 0.505423229336821 21.543404977178355 -1.0 -1.0 -1.0 35.0 888 288 +84 573.8507253223063 00:00:00 00:00:00 0.4967380348641639 11.084164653890928 -1.0 -1.0 -1.0 35.0 888 288 +85 420.6998183657156 00:00:00 00:00:00 0.45509970537168065 17.32850048985882 -1.0 -1.0 -1.0 35.0 888 288 +86 439.5439082870578 00:00:00 00:00:00 0.24326293577857605 35.833665311539534 -1.0 -1.0 -1.0 35.0 888 288 +88 450.20877052020955 00:00:00 00:00:00 0.2061046297575687 12.665759219918368 -1.0 -1.0 -1.0 35.0 888 288 +89 110.1657516020332 00:00:00 00:00:00 0.025901855591487423 18.200353247030144 -1.0 -1.0 -1.0 35.0 888 288 +90 443.8604241510417 00:00:00 00:00:00 0.3533866602834305 13.242335799793306 -1.0 -1.0 -1.0 35.0 888 288 +91 444.686556791516 00:00:00 00:00:00 0.4077832962117002 13.80369034602879 -1.0 -1.0 -1.0 35.0 888 288 +92 562.5781184397683 00:00:00 00:00:00 0.6835005902005468 10.27995831481763 -1.0 -1.0 -1.0 35.0 888 288 +93 167.62653574722364 00:00:00 00:00:00 0.02794717342101584 17.570916297751136 -1.0 -1.0 -1.0 35.0 888 288 +94 481.62428400668705 00:00:00 00:00:00 0.4565401559009469 11.826070143713975 -1.0 -1.0 -1.0 35.0 888 288 +95 475.5794761937849 00:00:00 00:00:00 0.5875361292861063 12.987091653999931 -1.0 -1.0 -1.0 35.0 888 288 +96 295.3711932454349 00:00:00 00:00:00 0.24218053028978936 11.871291318754192 -1.0 -1.0 -1.0 35.0 888 288 +98 584.9391974583868 00:00:00 00:00:00 0.6295707009501357 10.372529937559637 -1.0 -1.0 -1.0 35.0 888 288 +99 1076.886030040255 00:00:00 00:00:00 1.0553245057067546 14.901426606089192 -1.0 -1.0 -1.0 35.0 888 288 diff --git a/papers/lsst/sim_pz.py b/papers/lsst/sim_pz.py index 27a2cfba..0c2edc08 100644 --- a/papers/lsst/sim_pz.py +++ b/papers/lsst/sim_pz.py @@ -27,6 +27,8 @@ from scipy.interpolate import CubicSpline from scipy import stats import matplotlib +import importlib.resources as resources + defaultsize=18 ds=4 @@ -37,9 +39,11 @@ #r-band limits 24.7, 27.5(single visit, 10 year, these are 5 sigma limits) -def main(opdir="Data/"): +def main(): plotdir="Plots/" + opdir="Data/" + optdir = str(resources.files('zdm').joinpath('data/optical'))+"/" meerkat_z,meerkat_mr,meerkat_w = read_meerkat() @@ -115,21 +119,20 @@ def main(opdir="Data/"): fz0[i] = norm.cdf(Rlim0) fz1[i] = norm.cdf(Rlim1) fz2[i] = norm.cdf(Rlim2) - np.save(opdir+"fz_19.8.npy",fz0) - np.save(opdir+"fz_24.7.npy",fz1) - np.save(opdir+"fz_27.5.npy",fz2) + np.save(optdir+"fz_19.8.npy",fz0) + np.save(optdir+"fz_24.7.npy",fz1) + np.save(optdir+"fz_27.5.npy",fz2) np.save(opdir+"Rhist.npy",Rhist) np.save(opdir+"Rvals.npy",Rvals) np.save(opdir+"Rbars.npy",Rbars) else: - fz0 = np.load(opdir+"fz_19.8.npy") - fz1 = np.load(opdir+"fz_24.7.npy") - fz2 = np.load(opdir+"fz_27.5.npy") + fz0 = np.load(optdir+"fz_19.8.npy") + fz1 = np.load(optdir+"fz_24.7.npy") + fz2 = np.load(optdir+"fz_27.5.npy") Rhist = np.load(opdir+"Rhist.npy") Rvals = np.load(opdir+"Rvals.npy") Rbars = np.load(opdir+"Rbars.npy") - plt.figure() plt.xlabel("z") plt.ylabel("fraction visible") @@ -187,6 +190,9 @@ def main(opdir="Data/"): pz2 = pz*fz2 np.save(opdir+"zvals.npy",g.zvals) + np.save(optdir+"z_19.8.npy",g.zvals) + np.save(optdir+"z_24.7.npy",g.zvals) + np.save(optdir+"z_27.5.npy",g.zvals) np.save(opdir+prefixes[i]+"_pz.npy",pz) np.save(opdir+prefixes[i]+"_fpz0.npy",pz0) np.save(opdir+prefixes[i]+"_fpz1.npy",pz1) diff --git a/zdm/grid.py b/zdm/grid.py index b2d5dff3..5b3683df 100644 --- a/zdm/grid.py +++ b/zdm/grid.py @@ -39,7 +39,7 @@ from zdm import io import time import warnings - +import importlib.resources as resources class Grid: """2D grid for computing FRB detection rates as a function of z and DM. @@ -541,17 +541,20 @@ def calc_rates(self): exit() if self.survey.survey_data.observing.Z_FRACTION is not None: - sdir = os.path.join(resource_filename('zdm', 'data'), 'Optical') - self.linear_interpolation(np.load(sdir+"/fz_24.7.npy"),np.load(sdir+"/zvals.npy"),self.survey.survey_data.observing.Z_FRACTION) - self.fz=np.load(sdir+"/"+self.survey.survey_data.observing.Z_FRACTION+"_fz.npy") - self.sfr*=self.fz + fdir = str(resources.files('zdm').joinpath('data/optical')) + ffile = fdir + "/fz_"+str(self.survey.survey_data.observing.Z_FRACTION)+".npy" + zfile = fdir + "/z_"+str(self.survey.survey_data.observing.Z_FRACTION)+".npy" + self.construct_fz(ffile,zfile) + self.sfr *= self.fz self.sfr_smear = np.multiply(self.smear_grid.T, self.sfr).T - + + # below could pass more parameters internally, but this may not + # be the final implementation self.rates = self.pdv * self.sfr_smear - - if self.state.photo.smearing is True: - self.smear_z(self.rates) + self.zsigma = self.survey.survey_data.observing.Z_PHOTO + if self.zsigma > 0.: + self.smear_zgrid = self.smear_z(self.rates,self.zsigma) self.rates=self.smear_zgrid def get_rates(self): @@ -786,9 +789,13 @@ def initMC(self): pdv = np.multiply(wb_fraction.T, self.dV).T rate = pdv * self.sfr_smear - if self.state.photo.smearing is True: - self.smear_z(rate) - rate=np.copy(self.smear_zgrid) + # We do not implement photo-z smearing here + # the MC generates truth values of parameters + # smearing can be done very simple afterwards + # this smears the + #if self.survey.observing.Z_PHOTO > 1.: + # rate = self.smear_z(rate,self.survey.observing.Z_PHOTO) + # #rate=np.copy(self.smear_zgrid) rates.append(rate) pwb[i * nw + j] = np.sum(rate) @@ -1249,28 +1256,52 @@ def chk_upd_param(self, param: str, vparams: dict, update=False): # return updated - def smear_z(self,array): + def smear_z(self,array,zsigma): + """ + smears the z-grid according to a specified photometric error + """ r,c=array.shape - sigma=self.state.photo.sigma*len(self.zvals)/max(self.zvals) + # get sigma in grid units + sigma=zsigma/(self.dz) smear_size=int(self.state.photo.sigma_width*sigma) smear_size=smear_size-smear_size%2+1 smear_arr=np.linspace(-(smear_size-1)/2,(smear_size-1)//2,smear_size) + + # makes the approximation of taking the central value in the bin. smear_arr=np.exp(-(smear_arr**2)/(2*(sigma**2))) - #smear_arr=random.normal(loc=1,scale=self.state.photo.sigma,size=(smear_size)) + #normalise smear_arr/=np.sum(smear_arr) - if not hasattr(self,"smear_zgrid"): - self.smear_zgrid=np.zeros([r,c]) + + smear_zgrid=np.zeros([r,c]) for i in range(c): - self.smear_zgrid[:,i]=np.convolve(array[:,i],smear_arr,mode="same") - - def linear_interpolation(self,fz,z,name): - path = os.path.join(resource_filename('zdm', 'data'), 'Optical') - newz=np.copy(self.zvals) - newfz=np.zeros(len(newz)) - for i in range(len(newz)): - j2=np.where(z>newz[i])[0][0] - j1=j2+1 - newfz[i]=fz[j1]+(newz[i]-z[j1])*(fz[j2]-fz[j1])/(z[j2]-z[j1]) - np.save(path+"/"+name+"_fz",newfz) - np.save(path+"/"+name+"_z",newz) + smear_zgrid[:,i]=np.convolve(array[:,i],smear_arr,mode="same") + + return smear_zgrid + + def construct_fz(self,ffile,zfile): + """ + linearly interpolates passed fz values onto own zvals array + + Args: + ffile (string): file containing fraction of hosts seen at given redshift + zfile(string): file containing z values of above + """ + fz = np.load(ffile) + z = np.load(zfile) + + from scipy.interpolate import interp1d + f=interp1d(z,fz,kind="linear",bounds_error=False) + newfz=f(self.zvals) + + # check for unphysical values + toolow = np.where(newfz < 0.) + newfz[toolow] = 0 + toohigh = np.where(newfz > 1.) + newfz[toohigh] = 1. + + self.fz = newfz + + + #np.save(path+"/"+name+"_fz",newfz) + #np.save(path+"/"+name+"_z",newz) diff --git a/zdm/iteration.py b/zdm/iteration.py index cb5b9057..d2384089 100644 --- a/zdm/iteration.py +++ b/zdm/iteration.py @@ -1139,7 +1139,7 @@ def calc_likelihoods_2D(grid,survey,doplot=False,norm=True,psnr=True,printit=Fal psnr += differential*survey.beam_o[i]*usew ###### Breaks p(snr,b,w) into three components, and saves them ##### - # this allows comoutations of psnr given b and w values, collapsing these over the dimensions of b and w + # this allows computations of psnr given b and w values, collapsing these over the dimensions of b and w if pwb: # psnr given beam, width, z,dm diff --git a/zdm/scripts/H0_scan/run_H0_slice.py b/zdm/scripts/H0_scan/run_H0_slice.py deleted file mode 100644 index 71597a18..00000000 --- a/zdm/scripts/H0_scan/run_H0_slice.py +++ /dev/null @@ -1,238 +0,0 @@ -import argparse -import numpy as np -import os - -from zdm import figures -from zdm import iteration as it - -from zdm import parameters -from zdm import repeat_grid as zdm_repeat_grid -from zdm import MCMC -from zdm import survey -from astropy.cosmology import Planck18 - -from numpy import random -import matplotlib.pyplot as plt -import time - -def main(): - - t0 = time.time() - parser = argparse.ArgumentParser() - #parser.add_argument(dest='param',type=str,help="Parameter to do the slice in") - parser.add_argument(dest='min',type=float,help="Min value") - parser.add_argument(dest='max',type=float,help="Max value") - parser.add_argument('-f', '--files', default=None, nargs='+', type=str, help="Survey file names") - parser.add_argument('-s', '--smeared_surveys', default=None, nargs='+', type=str, help="Surveys with smeared z-vals") - parser.add_argument('-z', '--frac_surveys', default=None, nargs='+', type=str, help="Surveys with z-fraction") - parser.add_argument('-n',dest='n',type=int,default=50,help="Number of values") - # parser.add_argument('-r',dest='repeaters',default=False,action='store_true',help="Surveys are repeater surveys") - args = parser.parse_args() - - vals = np.linspace(args.min, args.max, args.n) - - # Set state - state = parameters.State() - state.set_astropy_cosmo(Planck18) - # param_dict={'sfr_n': 1.13, 'alpha': 1.5, 'lmean': 2.27, 'lsigma': 0.55, - # 'lEmax': 41.26, 'lEmin': 39.5, 'gamma': -0.95, 'H0': 73, - # 'min_lat': 0.0, 'sigmaDMG': 0.0, 'sigmaHalo': 20.0} - # param_dict={'sfr_n': 0.8808527057055584, 'alpha': 0.7895161131856694, - # 'lmean': 2.1198711983468064, 'lsigma': 0.44944780033763343, - # 'lEmax': 41.18671139482926, 'lEmin': 39.81049090314043, 'gamma': -1.1558450520609953, - # 'H0': 54.6887137195215, 'halo_method': 0, 'sigmaDMG': 0.0, 'sigmaHalo': 0.0, 'min_lat': 30.0} - param_dict={'sfr_n': 1.7294049204398037, 'alpha': 1.4859524003747502, - 'lmean': 2.3007428869522486, 'lsigma': 0.396300210604263, - 'lEmax': 41.0, 'lEmin': 38.35533894604933, 'gamma': 0.6032500201815869, - 'H0': 70.51322705185869, 'DMhalo': 39.800465306883666} - # param_dict={'lEmax': 40.578551786703116} - state.update_params(param_dict) - - state.update_param('Rgamma', -2.2) - state.update_param('lRmax', 3.0) - state.update_param('lRmin', -4.0) - #state.update_param('min_lat', 30.0) - - - # Initialise surveys - surveys_sep = [[], []] - s_surveys_sep=[[],[]] - - grid_params = {} - grid_params['dmmax'] = 7000.0 - grid_params['ndm'] = 1400 - grid_params['nz'] = 500 - ddm = grid_params['dmmax'] / grid_params['ndm'] - dmvals = (np.arange(grid_params['ndm']) + 1) * ddm - - if args.files is not None: - for survey_name in args.files: - s = survey.load_survey(survey_name, state, dmvals) - surveys_sep[0].append(s) - t1 = time.time() - print("Step 1: ", str(t1-t0), flush=True) - - # state.update_param('halo_method', 1) - # state.update_param(args.param, vals[0]) - - outdir = 'cube/' + 'H0' + '/' - if not os.path.exists(outdir): - os.makedirs(outdir) - - ll_lists = [] - for val in vals: - print("val:", val) - param = {"H0": {'min': -np.inf, 'max': np.inf}} - ll=0 - ll_list=[] - for i, surveys in enumerate(args.files): - state.photo.smearing=False - surveys_sep[0][i].survey_data.observing.Z_FRACTION=None - if args.smeared_surveys is not None: - if surveys in args.smeared_surveys: - state.photo.smearing=True - if args.frac_surveys is not None: - if surveys in args.frac_surveys: - surveys_sep[0][i].survey_data.observing.Z_FRACTION="lsst_24.7" - sll, sll_list = MCMC.calc_log_posterior([val], state, param,[[surveys_sep[0][i]],[]], grid_params, ind_surveys=True)#,psnr=True) - for s in sll_list: - ll_list.append(s) - ll+=sll - print(ll, ll_list) - ll_lists.append(ll_list) - t2 = time.time() - print("Step 2: ", str(t2-t1), flush=True) - t1 = t2 - print(ll_lists) - ll_lists = np.asarray(ll_lists) - - plt.figure() - linestyles=["-","--",":","-."] - s_names=["None","Photometric","$F_z$","Both"] - plt.clf() - llsum = np.zeros(ll_lists.shape[0]) - surveys = surveys_sep[0] + surveys_sep[1] - FWHM=[] - for i in range(len(surveys)): - s = surveys[i] - lls = ll_lists[:, i] - - lls[lls < -1e10] = -np.inf - lls[np.argwhere(np.isnan(lls))] = -np.inf - - llsum += lls - - lls = lls - np.max(lls) - lls=10**lls - index1=np.where(lls>=0.5)[0][0] - index2=np.where(lls>=0.5)[0][-1] - root1=vals[index1-1]-(0.5-lls[index1-1])*(vals[index1]-vals[index1-1])/(lls[index1]-lls[index1-1]) - root2=vals[index2]-(0.5-lls[index2])*(vals[index2+1]-vals[index2])/(lls[index2+1]-lls[index2]) - FWHM.append(root2-root1) - # plt.figure() - # plt.clf() - plt.plot(vals, lls, label=s_names[i],ls=linestyles[i]) - plt.xlabel('$H_0 (km s^{-1} Mpc^{-1})$',fontsize=14) - plt.ylabel('$\\frac{\\mathcal{L}}{max(\\mathcal{L})}$',fontsize=14) - # plt.savefig(os.path.join(outdir, s.name + ".pdf")) - print("Max H0:",vals[np.where(lls==1.0)[0][0]]) - - plt.minorticks_on() - plt.tick_params(axis='y', which='major', labelsize=14) # To set tick label fontsize - plt.tick_params(axis='y', which='major', length=9) # To set tick size - plt.tick_params(axis='y', which='minor', length=4.5) # To set tick size - plt.tick_params(axis='y', which='both',direction='in',right='on', top='on') - - plt.tick_params(axis='x', which='major', labelsize=14) # To set tick label fontsize - plt.tick_params(axis='x', which='major', length=9) # To set tick size - plt.tick_params(axis='x', which='minor', length=4.5) # To set tick size - plt.tick_params(axis='x', which='both',direction='in',right='on', top='on') - - print(vals) - print(llsum) - peak=vals[np.argwhere(llsum == np.max(llsum))[0]] - print("peak", peak,) - #plt.axvline(peak,ls='--') - plt.legend(fontsize=14,loc='upper left') - plt.savefig(outdir + "H0" + ".pdf") - percentage=(FWHM/FWHM[0]-1)*100 - print("FWHM:Spectroscopic,Photometric,zFrac,Photometric+zfrac\n",FWHM,percentage) - - # llsum = llsum - np.max(llsum) - # llsum[llsum < -1e10] = -np.inf - plt.figure() - plt.clf() - plt.plot(vals, llsum, label='Total') - plt.axvline(peak,ls='--') - # plt.plot(vals, llsum2) - plt.xlabel('H0') - plt.ylabel('log likelihood') - plt.legend() - plt.savefig(outdir + "H0"+ "_sum.pdf") - -#============================================================================== -""" -Function: plot_grids -Date: 10/01/2024 -Purpose: - Plot grids. Adapted from zdm/scripts/plot_pzdm_grid.py - -Imports: - grids = list of grids - surveys = list of surveys - outdir = output directory - val = parameter value for this grid -""" -def plot_grids(grids, surveys, outdir, val): - for g,s in zip(grids, surveys): - zvals=[] - dmvals=[] - nozlist=[] - - if s.zlist is not None: - for iFRB in s.zlist: - zvals.append(s.Zs[iFRB]) - dmvals.append(s.DMEGs[iFRB]) - if s.nozlist is not None: - for dm in s.DMEGs[s.nozlist]: - nozlist.append(dm) - - frbzvals = np.array(zvals) - frbdmvals = np.array(dmvals) - - figures.plot_grid( - g.rates, - g.zvals, - g.dmvals, - name=outdir + s.name + "_" + str(val) + ".pdf", - norm=3, - log=True, - label="$\\log_{10} p({\\rm DM}_{\\rm EG},z)$ [a.u.]", - project=False, - FRBDM=frbdmvals, - FRBZ=frbzvals, - Aconts=[0.01, 0.1, 0.5], - zmax=1.5, - DMmax=3000, - # DMlines=nozlist, - ) - -#============================================================================== -""" -Function: commasep -Date: 23/08/2022 -Purpose: - Turn a string of variables seperated by commas into a list - -Imports: - s = String of variables - -Exports: - List conversion of s -""" -def commasep(s): - return list(map(str, s.split(','))) - -#============================================================================== - -main() diff --git a/zdm/survey.py b/zdm/survey.py index 0379f753..291f3cc7 100644 --- a/zdm/survey.py +++ b/zdm/survey.py @@ -130,6 +130,7 @@ def __init__(self, state, survey_name: str, # Load up self.process_survey_file(filename, NFRB, iFRB, min_lat=state.analysis.min_lat, dmg_cut=state.analysis.DMG_cut,survey_dict = survey_dict) + # Check if repeaters or not and set relevant parameters # Now done in loading # self.repeaters=False diff --git a/zdm/survey_data.py b/zdm/survey_data.py index 73399c7e..27b40931 100644 --- a/zdm/survey_data.py +++ b/zdm/survey_data.py @@ -299,11 +299,14 @@ class Observing(data_class.myDataClass): 'unit': 'pc/cm**3', 'Notation': '', }) - Z_FRACTION:str =field( default=None, metadata={'help':"Fraction of visible FRBs at a redshift"} ) + Z_PHOTO:float =field( + default=0., + metadata={'help':"Gaussian photometric error on redshifts"} + ) class SurveyData(data_class.myData): """ Hold the SurveyData in a convenient object From 6b965bac20d894b8a6320ce952d19cefdf9607b4 Mon Sep 17 00:00:00 2001 From: Clancy James Date: Wed, 14 Jan 2026 09:55:06 +0800 Subject: [PATCH 12/35] minor updates prior to circulating lsst paper, and ref changes to scattering paper --- papers/Scattering/fit_scattering_width.py | 1484 +++++++++++++-------- papers/lsst/Photometric/plot_2dgrids.py | 10 +- papers/lsst/Photometric/plot_H0_slice.py | 2 +- papers/lsst/sim_pz.py | 10 +- zdm/figures.py | 4 +- 5 files changed, 909 insertions(+), 601 deletions(-) diff --git a/papers/Scattering/fit_scattering_width.py b/papers/Scattering/fit_scattering_width.py index af1623de..1fc58218 100644 --- a/papers/Scattering/fit_scattering_width.py +++ b/papers/Scattering/fit_scattering_width.py @@ -99,23 +99,118 @@ # CHIME values # raw were +# mu_tau, sigmatau in ln and 600 was: 2.02 ms, in log10 that's 0.3. Scaled to 1 GHz from 600 MHz is down by 0.6^4 = -0.88 so basically -0.57 +# sigma_tau at 600 was: 1.72. * log10(e) gives 0.75 + +# for width, it's 1.0 ms, 0.97, which is frequency independent, and becomes 0 + # scaled to 1GHz and log10 are # $(\mu_w,\sigma_w) = (\log_{10} 1.0 {\rm ms},0.42)$ # $(\mu_\tau,\sigma_\tau) = (\log_{10} 0.262 {\rm ms},0.75) CHIME_muw = 0. CHIME_sw = 0.42 -CHIME_mut = 0.3 + +CHIME_mut = 0.3 # scaled to 1 GHz is -0.58 CHIME_st = 0.75 -def main(outdir="Fitting_Outputs/"): - # does k-s test to scattering distribution + + +def plot_good_alpha(outdir,alphas,alphaerr,tauobs,tauobserr): + """ + makes diagnostic plot of good alpha values + """ + + OK1 = np.array(np.where(alphaerr < 1.)[0]) + + bins = np.linspace(-6,0,13) + plt.figure() + plt.xlabel("$\\alpha$") + plt.ylabel("$N(\\alpha)$") + plt.hist(alphas[OK1],bins=bins) + plt.tight_layout() + plt.savefig(outdir+"alphahist1.png") + plt.close() + + OK2 = np.where(np.abs(tauobserr[OK1]/tauobs[OK1])<0.1)[0] + OK3 = OK1[OK2] + + plt.figure() + plt.xlabel("$\\alpha$") + plt.ylabel("$N(\\alpha)$") + plt.hist(alphas[OK3],bins=bins) + plt.tight_layout() + plt.savefig(outdir+"alphahist3.png") + plt.close() + + plt.xlabel("$\\alpha$") + plt.ylabel("$\\sigma_{\\rm alpha}$") + plt.scatter(alphas,alphaerr) + plt.tight_layout() + plt.savefig(outdir+"alpha_err_scat.png") + plt.close() + + plt.xlabel("$\\alpha$") + plt.ylabel("$\\sigma_{\\rm alpha}$") + plt.scatter(alphas[OK3],alphaerr[OK3]) + plt.tight_layout() + plt.savefig(outdir+"alpha_err_scat3.png") + plt.close() + + + plt.xlabel("$\\tau$ [ms]") + plt.ylabel("$\\sigma_{\\rm tau}/\\tau$") + plt.xscale("log") + plt.yscale("log") + plt.scatter(tauobs,tauobserr/tauobs) + plt.tight_layout() + plt.savefig(outdir+"tau_obs_err.png") + plt.close() + +def fit_scat_width(outdir="Fitting_Outputs/",bootstrap=False,alpha=-4,doplots=False,bsalpha=False): + """ + + + Args: + outdir (string): directory to send outputs to + bootstrap (bool): add random errors to tau + alpha (float): use this as standard value of nu^\alpha, except if bsalpha is True + doplots (bool): generate publication plots + bsalpha (bool): add random variation to individual alphas + """ + + # recordss this value + mean_alpha=alpha if not os.path.exists(outdir): os.mkdir(outdir) - plot_functions(outdir=outdir) + if doplots: + plot_functions(outdir=outdir) + + tns,tauobs,w95,wsnr,z,snr,freq,DM,tres,tauobserr,taualpha,taualphaerr = get_data() + + + if doplots: + plot_good_alpha(outdir,taualpha,taualphaerr,tauobs,tauobserr) + - tns,tauobs,w95,wsnr,z,snr,freq,DM,tres = get_data() + # performs a bootstrap step to estimate uncertainties + if bootstrap: + tausim = tauobs + tauobserr*np.random.normal(0,1,tauobserr.size) + # resamples things which are -ve + while True: + neg = np.where(tausim < 0.)[0] + if len(neg)==0: + break + tausim[neg] = tauobs[neg]*np.random.normal(0,1,len(neg)) + tauobs = tausim + + if bsalpha: + # samples according to error for good measurements, and ~3 for poor ones + OK = np.where(taualphaerr < 1.)[0] + BAD = np.where(taualphaerr >= 1.)[0] + alpha = taualpha + taualphaerr*np.random.normal(0,1,taualpha.size) + alpha[BAD] = -np.random.normal(3,1,len(BAD)) # gets observed intrinsic widths wobs = wsnr**2 - (tauobs/0.816)**2 @@ -128,19 +223,20 @@ def main(outdir="Fitting_Outputs/"): wobs = wobs**0.5 # generates scatter plot of tau and width - plt.figure() - plt.scatter(wsnr,tauobs) - plt.xlabel("$w_{\\rm SNR}$") - plt.ylabel("$\\tau_{\\rm obs}$") - plt.xscale("log") - plt.yscale("log") - slope = 0.816 - x=np.array([1e-2,20]) - y=slope*x - plt.plot(x,y,linestyle="--",color="black") - plt.tight_layout() - plt.savefig(outdir+"scatter_w_tau.png") - plt.close() + if doplots: + plt.figure() + plt.scatter(wsnr,tauobs) + plt.xlabel("$w_{\\rm SNR}$") + plt.ylabel("$\\tau_{\\rm obs}$") + plt.xscale("log") + plt.yscale("log") + slope = 0.816 + x=np.array([1e-2,20]) + y=slope*x + plt.plot(x,y,linestyle="--",color="black") + plt.tight_layout() + plt.savefig(outdir+"scatter_w_tau.png") + plt.close() NFRB = snr.size @@ -157,9 +253,8 @@ def main(outdir="Fitting_Outputs/"): maxws[i] = wmax # scale taus to 1 GHz in host rest frame - alpha = -4 - host_tau = tauobs*(1+z)**3 * (freq/1e3)**-alpha - host_maxtaus = maxtaus * (1+z)**3 * (freq/1e3)**-alpha + host_tau = tauobs*(1+z)**(-alpha-1) * (freq/1e3)**-alpha + host_maxtaus = maxtaus * (1+z)**(-alpha-1) * (freq/1e3)**-alpha host_w = wobs/(1+z) host_maxw = maxws/(1+z) @@ -167,29 +262,25 @@ def main(outdir="Fitting_Outputs/"): # print(i,z[i],maxws[i],host_maxw[i]) - - - - - - # generates a table - print("Table of FRB properties for latex") - for i,name in enumerate(tns): - string=name - # detection properties - string += " & " + str(int(DM[i]+0.5)) + " & " + str(z[i])[0:6] + " & " + str(snr[i])[0:4] - string += " & " + str(freq[i])[0:6] + " & " + str(tres[i])[0:6] + " & " + str(w95[i])[0:4] - - # scattering - string += " & " + str(tauobs[i])[0:4] + " & " + str(maxtaus[i])[0:4] + " & " + str(host_tau[i])[0:4] + " & " + str(host_maxtaus[i])[0:4] - - # width - string += " & " + str(wobs[i])[0:4] + " & " + str(maxws[i])[0:4] + " & " + str(host_w[i])[0:4] + " & " + str(host_maxw[i])[0:4] - - # newline - string += " \\\\" - print(string) - print("\n\n\n\n") + if doplots: + # generates a table + print("Table of FRB properties for latex") + for i,name in enumerate(tns): + string=name + # detection properties + string += " & " + str(int(DM[i]+0.5)) + " & " + str(z[i])[0:6] + " & " + str(snr[i])[0:4] + string += " & " + str(freq[i])[0:6] + " & " + str(tres[i])[0:6] + " & " + str(w95[i])[0:4] + + # scattering + string += " & " + str(tauobs[i])[0:4] + " & " + str(maxtaus[i])[0:4] + " & " + str(host_tau[i])[0:4] + " & " + str(host_maxtaus[i])[0:4] + + # width + string += " & " + str(wobs[i])[0:4] + " & " + str(maxws[i])[0:4] + " & " + str(host_w[i])[0:4] + " & " + str(host_maxw[i])[0:4] + + # newline + string += " \\\\" + print(string) + print("\n\n\n\n") Nbins=11 bins = np.logspace(-3.,2.,Nbins) @@ -200,153 +291,158 @@ def main(outdir="Fitting_Outputs/"): ############## Observed Histogram ################# - plt.figure() - #obshist,bins = np.histogram(tauobs,bins=bins) - #hosthist,bins = np.histogram(host_tau,bins=bins) - ax1=plt.gca() - plt.xscale("log") - plt.ylim(0,8) - l1 = plt.hist(tauobs,bins=bins,label="Observed",alpha=0.5) - - # makes a function of completeness - xvals,yvals = make_completeness_plot(maxtaus) - - tau_comp = get_completeness(tauobs,xvals,yvals) - - l2 = plt.hist(tauobs,bins=bins,weights = 1./tau_comp,label="Observed",alpha=0.5) - - - ax2 = ax1.twinx() - l3 = ax2.plot(xvals,yvals,label="Completeness") - plt.ylim(0,1) - plt.xlim(1e-2,1e3) - plt.ylabel("Completeness") - plt.sca(ax1) - plt.legend(handles=[l1[2],l3[0],l2[2]],labels=["Observed","Completeness","Corrected"],fontsize=12) - plt.xlabel("$\\tau_{\\rm obs}$ [ms]") - plt.ylabel("Number of FRBs") - plt.text(2e-3,8,"(a)",fontsize=18) - plt.tight_layout() - plt.savefig(outdir+"tau_observed_histogram.png") - plt.close() - + # only plot this if doplotsping! + if doplots: + plt.figure() + #obshist,bins = np.histogram(tauobs,bins=bins) + #hosthist,bins = np.histogram(host_tau,bins=bins) + ax1=plt.gca() + plt.xscale("log") + plt.ylim(0,8) + l1 = plt.hist(tauobs,bins=bins,label="Observed",alpha=0.5) - - ############## 1 GHz Rest-frame Histogram ################# - - plt.figure() - #obshist,bins = np.histogram(tauobs,bins=bins) - #hosthist,bins = np.histogram(host_tau,bins=bins) - ax1=plt.gca() - plt.xscale("log") - plt.ylim(0,8) - l1 = plt.hist(host_tau,bins=bins,label="Observed",alpha=0.5) - - # makes a function of completeness - xvals,yvals = make_completeness_plot(host_maxtaus) - - # get completeness at points of measurement - tau_comp = get_completeness(host_tau,xvals,yvals) - - l2 = plt.hist(host_tau,bins=bins,weights = 1./tau_comp,label="Corrected",alpha=0.5) - - - ax2 = ax1.twinx() - l3 = ax2.plot(xvals,yvals,label="Completeness") - use_this_color = l3[0].get_color() - - plt.ylim(0,1) - plt.xlim(1e-2) - plt.ylabel("Completeness") - plt.sca(ax1) - plt.legend(handles=[l1[2],l3[0],l2[2]],labels=["Observed","Completeness","Corrected"],fontsize=12) - plt.xlabel("$\\tau_{\\rm host, 1\,GHz}$ [ms]") - plt.ylabel("Number of FRBs") - - plt.text(2e-3,8,"(b)",fontsize=18) - plt.tight_layout() - plt.savefig(outdir+"tau_host_histogram.png") - - #### creates a copy of the above, for paper purposes. Does this three times! Once - # for "everything", once for intrinsic, once for observed - - # "everything" figures # - plt.figure() - #obshist,bins = np.histogram(tauobs,bins=bins) - #hosthist,bins = np.histogram(host_tau,bins=bins) - ax1v2=plt.gca() - plt.xscale("log") - plt.ylim(0,8) - l1v2 = plt.hist(host_tau,bins=bins,label="Observed",alpha=0.5) - - # makes a function of completeness - xvals,yvals = make_completeness_plot(host_maxtaus) - - # get completeness at points of measurement - tau_comp = get_completeness(host_tau,xvals,yvals) - - l2v2 = plt.hist(host_tau,bins=bins,weights = 1./tau_comp,label="Corrected",alpha=0.5) - - - ax2v2 = ax1v2.twinx() - l3v2 = ax2v2.plot(xvals,yvals,label="Completeness") - - plt.ylim(0,1) - plt.xlim(1e-2) - plt.ylabel("Completeness") - plt.sca(ax1) - plt.xlabel("$\\tau_{\\rm host, 1\,GHz}$ [ms]") - plt.ylabel("Number of FRBs") - # keeps open for later plotting - don't close this here - - - # "observed" figures # - plt.figure() - #obshist,bins = np.histogram(tauobs,bins=bins) - #hosthist,bins = np.histogram(host_tau,bins=bins) - ax1v3=plt.gca() - plt.xscale("log") - plt.ylim(0,8) - l1v3 = plt.hist(host_tau,bins=bins,label="Observed",alpha=0.5) - - ax2v3 = ax1v3.twinx() - l3v3 = ax2v3.plot(xvals,yvals,label="Completeness") - - plt.ylim(0,1) - plt.xlim(1e-2) - plt.ylabel("Completeness") - plt.sca(ax1) - plt.xlabel("$\\tau_{\\rm host, 1\,GHz}$ [ms]") - plt.ylabel("Number of FRBs") - # keeps open for later plotting - don't close this here - - # "intrinsic" figures # - plt.figure() - #obshist,bins = np.histogram(tauobs,bins=bins) - #hosthist,bins = np.histogram(host_tau,bins=bins) - ax1v4=plt.gca() - plt.xscale("log") - plt.ylim(0,8) - l2v4 = plt.hist(host_tau,bins=bins,weights = 1./tau_comp,label="Corrected",alpha=0.5) - - - ax2v4 = ax1v4.twinx() - l3v4 = ax2v4.plot(xvals,yvals,label="Completeness") - - plt.ylim(0,1) - plt.xlim(1e-2) - plt.ylabel("Completeness") - plt.sca(ax1) - plt.xlabel("$\\tau_{\\rm host, 1\,GHz}$ [ms]") - plt.ylabel("Intrinsic number of FRBs") - # keeps open for later plotting - don't close this here + # makes a function of completeness + xvals,yvals = make_completeness_plot(maxtaus) + + tau_comp = get_completeness(tauobs,xvals,yvals) + + l2 = plt.hist(tauobs,bins=bins,weights = 1./tau_comp,label="Observed",alpha=0.5) + + + ax2 = ax1.twinx() + l3 = ax2.plot(xvals,yvals,label="Completeness") + plt.ylim(0,1) + plt.xlim(1e-2,1e3) + plt.ylabel("Completeness") + plt.sca(ax1) + plt.legend(handles=[l1[2],l3[0],l2[2]],labels=["Observed","Completeness","Corrected"],fontsize=12) + plt.xlabel("$\\tau_{\\rm obs}$ [ms]") + plt.ylabel("Number of FRBs") + plt.text(2e-3,8,"(a)",fontsize=18) + plt.tight_layout() + plt.savefig(outdir+"tau_observed_histogram.png") + plt.close() + + + + ############## 1 GHz Rest-frame Histogram ################# + + plt.figure() + #obshist,bins = np.histogram(tauobs,bins=bins) + #hosthist,bins = np.histogram(host_tau,bins=bins) + ax1=plt.gca() + plt.xscale("log") + plt.ylim(0,8) + l1 = plt.hist(host_tau,bins=bins,label="Observed",alpha=0.5) + + # makes a function of completeness + xvals,yvals = make_completeness_plot(host_maxtaus) + + # get completeness at points of measurement + tau_comp = get_completeness(host_tau,xvals,yvals) + + l2 = plt.hist(host_tau,bins=bins,weights = 1./tau_comp,label="Corrected",alpha=0.5) + + + ax2 = ax1.twinx() + l3 = ax2.plot(xvals,yvals,label="Completeness") + use_this_color = l3[0].get_color() + + plt.ylim(0,1) + plt.xlim(1e-2) + plt.ylabel("Completeness") + plt.sca(ax1) + plt.legend(handles=[l1[2],l3[0],l2[2]],labels=["Observed","Completeness","Corrected"],fontsize=12) + plt.xlabel("$\\tau_{\\rm host, 1\,GHz}$ [ms]") + plt.ylabel("Number of FRBs") + + plt.text(2e-3,8,"(b)",fontsize=18) + plt.tight_layout() + plt.savefig(outdir+"tau_host_histogram.png") + + #### creates a copy of the above, for paper purposes. Does this three times! Once + # for "everything", once for intrinsic, once for observed + + # "everything" figures # + plt.figure() + #obshist,bins = np.histogram(tauobs,bins=bins) + #hosthist,bins = np.histogram(host_tau,bins=bins) + ax1v2=plt.gca() + plt.xscale("log") + plt.ylim(0,8) + l1v2 = plt.hist(host_tau,bins=bins,label="Observed",alpha=0.5) + + # makes a function of completeness + xvals,yvals = make_completeness_plot(host_maxtaus) + + # get completeness at points of measurement + tau_comp = get_completeness(host_tau,xvals,yvals) + + l2v2 = plt.hist(host_tau,bins=bins,weights = 1./tau_comp,label="Corrected",alpha=0.5) + + + ax2v2 = ax1v2.twinx() + l3v2 = ax2v2.plot(xvals,yvals,label="Completeness") + + plt.ylim(0,1) + plt.xlim(1e-2) + plt.ylabel("Completeness") + plt.sca(ax1) + plt.xlabel("$\\tau_{\\rm host, 1\,GHz}$ [ms]") + plt.ylabel("Number of FRBs") + # keeps open for later plotting - don't close this here + + + # "observed" figures # + plt.figure() + #obshist,bins = np.histogram(tauobs,bins=bins) + #hosthist,bins = np.histogram(host_tau,bins=bins) + ax1v3=plt.gca() + plt.xscale("log") + plt.ylim(0,8) + l1v3 = plt.hist(host_tau,bins=bins,label="Observed",alpha=0.5) + + ax2v3 = ax1v3.twinx() + l3v3 = ax2v3.plot(xvals,yvals,label="Completeness") + + plt.ylim(0,1) + plt.xlim(1e-2) + plt.ylabel("Completeness") + plt.sca(ax1) + plt.xlabel("$\\tau_{\\rm host, 1\,GHz}$ [ms]") + plt.ylabel("Number of FRBs") + # keeps open for later plotting - don't close this here + + # "intrinsic" figures # + plt.figure() + #obshist,bins = np.histogram(tauobs,bins=bins) + #hosthist,bins = np.histogram(host_tau,bins=bins) + ax1v4=plt.gca() + plt.xscale("log") + plt.ylim(0,8) + l2v4 = plt.hist(host_tau,bins=bins,weights = 1./tau_comp,label="Corrected",alpha=0.5) + + + ax2v4 = ax1v4.twinx() + l3v4 = ax2v4.plot(xvals,yvals,label="Completeness") + + plt.ylim(0,1) + plt.xlim(1e-2) + plt.ylabel("Completeness") + plt.sca(ax1) + plt.xlabel("$\\tau_{\\rm host, 1\,GHz}$ [ms]") + plt.ylabel("Intrinsic number of FRBs") + # keeps open for later plotting - don't close this here ####################################### TAU - CDF and fitting ################################ - print("\n\n KS test evaluation for tau \n") + if doplots: + print("\n\n KS test evaluation for tau \n") # amplitude, mean, and std dev of true distribution + xvals,yvals = make_completeness_plot(host_maxtaus) + ksbest = [] # begins minimisation for KS statistic for ifunc in np.arange(NFUNC): @@ -358,9 +454,10 @@ def main(outdir="Fitting_Outputs/"): psub1 = get_ks_stat(result.x,*args,plot=False) ksbest.append(result.x) #Best-fitting parameters are [0.85909445 1.45509687] with p-value 0.9202915513749959 - print("FUNCTION ",ifunc,",",FNAMES[ifunc]," Best-fitting parameters are ",result.x," with p-value ",1.-result.fun) + if doplots: + print("FUNCTION ",ifunc,",",FNAMES[ifunc]," Best-fitting parameters are ",result.x," with p-value ",1.-result.fun) - + # adds an extra couple of points here. For.. reasons? xtemp = xvals[::2] ytemp = yvals[::2] s = xtemp.size @@ -368,17 +465,17 @@ def main(outdir="Fitting_Outputs/"): yt = np.zeros([s+2]) xt[0] = xtemp[0] yt[0] = ytemp[0] - xt[1] = 1. - yt[1] = 1. xt[2:-1] = xtemp[1:] yt[2:-1] = ytemp[1:] + xt[1] = 0.75*xt[2] + yt[1] = 1. xt[-1] = 1e5 yt[-1] = 0. cspline = sp.interpolate.make_interp_spline(np.log10(xt), yt,k=1) - # get a spline interpolation of completeness. Should be removed from function! + # get a spline interpolation of completeness. Should be removed from function! # do a test plot of the spline - if True: + if doplots: plt.figure() plt.plot(np.logspace(-5,5,101),cspline(np.linspace(-5,5,101))) plt.plot(xvals,yvals) @@ -390,9 +487,11 @@ def main(outdir="Fitting_Outputs/"): make_cdf_plot(ksbest,host_tau,xvals,yvals,outdir+"bestfit_ks_scat_cumulative.png",cspline) ############################################### TAU - likelihood analysis ################################################# - print("\n\n Max Likelihood Calculation for tau\n") + if doplots: + print("\n\n Max Likelihood Calculation for tau\n") xbest=[] llbests = [] + pbests=[] for ifunc in np.arange(NFUNC): args = (host_tau,cspline,ifunc) x0=ARGS0[ifunc] @@ -403,135 +502,145 @@ def main(outdir="Fitting_Outputs/"): # llbest returns negative ll llbest = get_ll_stat(result.x,host_tau,cspline,ifunc) * -1 llbests.append(llbest) - - print("FUNCTION ",ifunc,",",FNAMES[ifunc]," Best-fitting log-likelihood parameters are ",result.x," with likelihood ",-result.fun) - print(" , BIC is ",2*np.log(host_tau.size) - len(x0)*llbest) + pbests.append(-result.fun) + if doplots: + print("FUNCTION ",ifunc,",",FNAMES[ifunc]," Best-fitting log-likelihood parameters are ",result.x," with likelihood ",-result.fun) + print(" , BIC is ",2*np.log(host_tau.size) - len(x0)*llbest) if ifunc == 0: - llCHIME = get_ll_stat([CHIME_mut,CHIME_st],host_tau,cspline,ifunc) * -1 - print("Compare with CHIME ",llCHIME) - - print("\n\nLATEX TABLE") - for ifunc in np.arange(NFUNC): - string = FNAMES[ifunc] + " & $" + latextau[ifunc] + f"$ & ${xbest[ifunc][0]:.2f}" - for iarg, arg in enumerate(xbest[ifunc]): - if iarg==0: - continue - string += f", {xbest[ifunc][iarg]:.2f}" - string += f" $ & {llbests[ifunc]:.2f} \\\\" - print(string) - print("\n\n\n\n") - - make_cdf_plot(xbest,host_tau,xvals,yvals,outdir+"bestfit_ll_scat_cumulative.png",cspline) + taullCHIME = get_ll_stat([CHIME_mut - np.log10(0.6**mean_alpha),CHIME_st],host_tau,cspline,ifunc) * -1 + if doplots: + print("Compare with CHIME ",taullCHIME) + taullCHIME -= llbests[3] + + tauxbest = xbest + taullbests = llbests + taupbests = pbests + + if doplots: + print("\n\nLATEX TABLE") + for ifunc in np.arange(NFUNC): + string = FNAMES[ifunc] + " & $" + latextau[ifunc] + f"$ & ${xbest[ifunc][0]:.2f}" + for iarg, arg in enumerate(xbest[ifunc]): + if iarg==0: + continue + string += f", {xbest[ifunc][iarg]:.2f}" + string += f" $ & {llbests[ifunc]:.2f} \\\\" + print(string) + print("\n\n\n\n") + + if doplots: + make_cdf_plot(xbest,host_tau,xvals,yvals,outdir+"bestfit_ll_scat_cumulative.png",cspline) ######## does plot with all fits added ######## - - plt.sca(ax1) - NFRB=host_tau.size - handles=[l1[2],l3[0],l2[2]] - labels=["Observed","Completeness","Corrected$"] - styles=["-.","--","--",":"] - for i in np.arange(NFUNC): - print("plotting function ",i," with xbest ",xbest[i]) - xs,ys = function_wrapper(i,xbest[i])#cspline=None): - plotnorm = NFRB * (np.log10(bins[1])-np.log10(bins[0])) - l=plt.plot(xs,ys*plotnorm,label=FNAMES[i]) - handles.append(l[0]) - labels.append(FNAMES[i]) - xs,ys = function_wrapper(i,xbest[i],cspline=cspline) - plt.plot(xs,ys*plotnorm,linestyle=":",color=plt.gca().lines[-1].get_color()) - plt.xscale("log") - plt.xlim(1e-2,1e3) - plt.legend() - - plt.legend(handles=handles,labels=labels,fontsize=6) #fontsize=12) - - - plt.savefig(outdir+"tau_host_histogram_fits.png") - plt.close() + if doplots: + plt.sca(ax1) + NFRB=host_tau.size + handles=[l1[2],l3[0],l2[2]] + labels=["Observed","Completeness","Corrected$"] + styles=["-.","--","--",":"] + for i in np.arange(NFUNC): + print("plotting function ",i," with xbest ",xbest[i]) + xs,ys = function_wrapper(i,xbest[i])#cspline=None): + plotnorm = NFRB * (np.log10(bins[1])-np.log10(bins[0])) + l=plt.plot(xs,ys*plotnorm,label=FNAMES[i]) + handles.append(l[0]) + labels.append(FNAMES[i]) + xs,ys = function_wrapper(i,xbest[i],cspline=cspline) + plt.plot(xs,ys*plotnorm,linestyle=":",color=plt.gca().lines[-1].get_color()) + plt.xscale("log") + plt.xlim(1e-2,1e3) + plt.legend() + + plt.legend(handles=handles,labels=labels,fontsize=6) #fontsize=12) + + + plt.savefig(outdir+"tau_host_histogram_fits.png") + plt.close() ######## plots for paper ######## - #"everything" - plt.sca(ax1v2) - NFRB=host_tau.size - handles=[l1v2[2],l3v2[0],l2v2[2]] - labels=["Observed","Completeness","Corrected"] - for i in [0,2,3]: - print("plotting function ",i," with xbest ",xbest[i]) - xs,ys = function_wrapper(i,xbest[i])#cspline=None): - plotnorm = NFRB * (np.log10(bins[1])-np.log10(bins[0])) - l=plt.plot(xs,ys*plotnorm,label=FNAMES[i]) - handles.append(l[0]) - labels.append(FNAMES[i]) - xs,ys = function_wrapper(i,xbest[i],cspline=cspline) - plt.plot(xs,ys*plotnorm,linestyle=":",color=plt.gca().lines[-1].get_color()) - plt.xscale("log") - plt.xlim(1e-2,1e3) - - plt.text(1e-3,8,"(b)",fontsize=18) - plt.legend() - plt.xlabel("$\\tau_{\\rm host, 1\,GHz}$ [ms]") - plt.ylabel("Number of FRBs") - plt.legend(handles=handles,labels=labels,fontsize=10) #fontsize=12) - - plt.tight_layout() - plt.savefig(outdir+"paper_tau_host_histogram_fits.png") - plt.close() - - #"observed" - plt.sca(ax1v3) - NFRB=host_tau.size - handles=[l1v3[2],l3v3[0]] - labels=["Observed","Completeness"] - for i in [0,2,3]: - print("plotting function ",i," with xbest ",xbest[i]) - xs,ys = function_wrapper(i,xbest[i],logxmax=2)#cspline=None): - plotnorm = NFRB * (np.log10(bins[1])-np.log10(bins[0])) - #l=plt.plot(xs,ys*plotnorm,label=FNAMES[i]) - - xs,ys = function_wrapper(i,xbest[i],cspline=cspline) - l=plt.plot(xs,ys*plotnorm,label=FNAMES[i],linestyle=styles[i]) - handles.append(l[0]) - labels.append(FNAMES[i]) - - plt.xscale("log") - plt.xlim(1e-2,1e3) - - plt.text(1e-3,8,"(a)",fontsize=18) - plt.legend() - plt.xlabel("$\\tau_{\\rm host, 1\,GHz}$ [ms]") - plt.ylabel("Observed number of FRBs") - plt.legend(handles=handles,labels=labels,fontsize=10) #fontsize=12) - - plt.tight_layout() - plt.savefig(outdir+"observed_paper_tau_host_histogram_fits.png") - plt.close() - - # "intrinsic" - plt.sca(ax1v4) - NFRB=host_tau.size - handles=[l2v4[2],l3v4[0]] - labels=["Adjusted","Completeness"] - for i in [0,2,3]: - print("plotting function ",i," with xbest ",xbest[i]) - xs,ys = function_wrapper(i,xbest[i],logxmax=2)#cspline=None): - plotnorm = NFRB * (np.log10(bins[1])-np.log10(bins[0])) - l=plt.plot(xs,ys*plotnorm,label=FNAMES[i],linestyle=styles[i]) - handles.append(l[0]) - labels.append(FNAMES[i]) - plt.xscale("log") - plt.xlim(1e-2,1e3) - - plt.text(1e-3,8,"(b)",fontsize=18) - plt.legend() - plt.xlabel("$\\tau_{\\rm host, 1\,GHz}$ [ms]") - plt.ylabel("Intrinsic number of FRBs") - plt.legend(handles=handles,labels=labels,fontsize=10) #fontsize=12) - - plt.tight_layout() - plt.savefig(outdir+"intrinsic_paper_tau_host_histogram_fits.png") - plt.close() + if doplots: + #"everything" + plt.sca(ax1v2) + NFRB=host_tau.size + handles=[l1v2[2],l3v2[0],l2v2[2]] + labels=["Observed","Completeness","Corrected"] + for i in [0,2,3]: + print("plotting function ",i," with xbest ",xbest[i]) + xs,ys = function_wrapper(i,xbest[i])#cspline=None): + plotnorm = NFRB * (np.log10(bins[1])-np.log10(bins[0])) + l=plt.plot(xs,ys*plotnorm,label=FNAMES[i]) + handles.append(l[0]) + labels.append(FNAMES[i]) + xs,ys = function_wrapper(i,xbest[i],cspline=cspline) + plt.plot(xs,ys*plotnorm,linestyle=":",color=plt.gca().lines[-1].get_color()) + plt.xscale("log") + plt.xlim(1e-2,1e3) + + plt.text(1e-3,8,"(b)",fontsize=18) + plt.legend() + plt.xlabel("$\\tau_{\\rm host, 1\,GHz}$ [ms]") + plt.ylabel("Number of FRBs") + plt.legend(handles=handles,labels=labels,fontsize=10) #fontsize=12) + + plt.tight_layout() + plt.savefig(outdir+"paper_tau_host_histogram_fits.png") + plt.close() + + #"observed" + plt.sca(ax1v3) + NFRB=host_tau.size + handles=[l1v3[2],l3v3[0]] + labels=["Observed","Completeness"] + for i in [0,2,3]: + print("plotting function ",i," with xbest ",xbest[i]) + xs,ys = function_wrapper(i,xbest[i],logxmax=2)#cspline=None): + plotnorm = NFRB * (np.log10(bins[1])-np.log10(bins[0])) + #l=plt.plot(xs,ys*plotnorm,label=FNAMES[i]) + + xs,ys = function_wrapper(i,xbest[i],cspline=cspline) + l=plt.plot(xs,ys*plotnorm,label=FNAMES[i],linestyle=styles[i]) + handles.append(l[0]) + labels.append(FNAMES[i]) + + plt.xscale("log") + plt.xlim(1e-2,1e3) + + plt.text(1e-3,8,"(a)",fontsize=18) + plt.legend() + plt.xlabel("$\\tau_{\\rm host, 1\,GHz}$ [ms]") + plt.ylabel("Observed number of FRBs") + plt.legend(handles=handles,labels=labels,fontsize=10) #fontsize=12) + + plt.tight_layout() + plt.savefig(outdir+"observed_paper_tau_host_histogram_fits.png") + plt.close() + + # "intrinsic" + plt.sca(ax1v4) + NFRB=host_tau.size + handles=[l2v4[2],l3v4[0]] + labels=["Adjusted","Completeness"] + for i in [0,2,3]: + print("plotting function ",i," with xbest ",xbest[i]) + xs,ys = function_wrapper(i,xbest[i],logxmax=2)#cspline=None): + plotnorm = NFRB * (np.log10(bins[1])-np.log10(bins[0])) + l=plt.plot(xs,ys*plotnorm,label=FNAMES[i],linestyle=styles[i]) + handles.append(l[0]) + labels.append(FNAMES[i]) + plt.xscale("log") + plt.xlim(1e-2,1e3) + + plt.text(1e-3,8,"(b)",fontsize=18) + plt.legend() + plt.xlabel("$\\tau_{\\rm host, 1\,GHz}$ [ms]") + plt.ylabel("Intrinsic number of FRBs") + plt.legend(handles=handles,labels=labels,fontsize=10) #fontsize=12) + + plt.tight_layout() + plt.savefig(outdir+"intrinsic_paper_tau_host_histogram_fits.png") + plt.close() ############################################# TAU - bayes factor ####################################### # priors @@ -541,10 +650,12 @@ def main(outdir="Fitting_Outputs/"): # min mean/min/max: size of distribution # NOTE: Bayes factor is P(data|model1)/P(data|model2) - print("\n\n Bayes Factor Calculation\n") + if doplots: + print("\n\n Bayes Factor Calculation\n") for ifunc in np.arange(NFUNC): if True: - print("skipping Bayes factor calculation for Tau, remove this line to re-run") + if doplots: + print("skipping Bayes factor calculation for Tau, remove this line to re-run") #FUNCTION 0 has likelihood sum 2.6815961887322472e-21 now compute Bayes factor! #FUNCTION 1 has likelihood sum 1.4462729739641349e-15 now compute Bayes factor! #FUNCTION 2 has likelihood sum 5.364450880196842e-16 now compute Bayes factor! @@ -552,7 +663,7 @@ def main(outdir="Fitting_Outputs/"): #FUNCTION 4 has likelihood sum 7.269113417017299e-16 now compute Bayes factor! #FUNCTION 5 has likelihood sum 6.248172055823414e-16 now compute Bayes factor! #FUNCTION 6 has likelihood sum 6.339647378300337e-16 now compute Bayes factor! - continue + break llsum=0. N1=100 @@ -589,7 +700,8 @@ def main(outdir="Fitting_Outputs/"): if (ifunc == 2 or ifunc == 4 or ifunc==5 or ifunc==6): llsum *= 2 #because the parameter space is actually half that calculated above - print("FUNCTION ",ifunc,",",FNAMES[ifunc]," has likelihood sum ",llsum, " now compute Bayes factor!") + if doplots: + print("FUNCTION ",ifunc,",",FNAMES[ifunc]," has likelihood sum ",llsum, " now compute Bayes factor!") @@ -598,141 +710,147 @@ def main(outdir="Fitting_Outputs/"): ############################################## WIDTH ############################################### ###################################################################################################### - print("\n\n\n\n######### WIDTH #########\n") + if doplots: + print("\n\n\n\n######### WIDTH #########\n") ######################################### Observed Histogram ############################################ - plt.figure() - ax1=plt.gca() - plt.xscale("log") - plt.ylim(0,8) - l1 = plt.hist(wobs,bins=bins,label="Observed",alpha=0.5) - - # makes a function of completeness - wxvals,wyvals = make_completeness_plot(maxws) - - wi_comp = get_completeness(wobs,wxvals,wyvals) - - l2 = plt.hist(wobs,bins=bins,weights = 1./wi_comp,label="Observed",alpha=0.5) - - - ax2 = ax1.twinx() - l3 = ax2.plot(wxvals,wyvals,label="Completeness") - plt.ylim(0,1) - plt.xlim(5e-3,1e2) - plt.ylabel("Completeness") - plt.sca(ax1) - plt.legend(handles=[l1[2],l3[0],l2[2]],labels=["Observed","Completeness","Corrected $w_i$"],fontsize=12) - plt.xlabel("$w_i$ [ms]") - plt.ylabel("Number of FRBs") - - plt.text(1e-3,8,"(a)",fontsize=18) - plt.tight_layout() - plt.savefig(outdir+"w_observed_histogram.png") - plt.close() - - ################################ 1 GHz Rest-frame Histogram ########################## - - plt.figure() - ax1=plt.gca() - plt.xscale("log") - plt.ylim(0,8) - l1 = plt.hist(host_w,bins=bins,label="Host",alpha=0.5) - - # makes a function of completeness - wxvals,wyvals = make_completeness_plot(host_maxw) - - # get completeness at points of measurement - w_comp = get_completeness(host_w,wxvals,wyvals) - - l2 = plt.hist(host_w,bins=bins,weights = 1./w_comp,label="Observed",alpha=0.5) - - - ax2 = ax1.twinx() - l3 = ax2.plot(wxvals,wyvals,label="Completeness") - - plt.ylim(0,1) - plt.xlim(5e-3,1e2) - plt.ylabel("Completeness") - plt.sca(ax1) - plt.legend(handles=[l1[2],l3[0],l2[2]],labels=["Observed","Completeness","Corrected"],fontsize=12) - plt.xlabel("$w_{i,\\rm host}$ [ms]") - plt.ylabel("Number of FRBs") - plt.tight_layout() - plt.savefig(outdir+"w_host_histogram.png") - - # keeps open for plotting later - - #### new plot, for paper - just a copy of the above #### - - #"everything" - plt.figure() - plt.xscale("log") - plt.ylim(0,8) - l1v2 = plt.hist(host_w,bins=bins,label="Host",alpha=0.5) + # only plot if doplotsping + if doplots: + plt.figure() + ax1=plt.gca() + plt.xscale("log") + plt.ylim(0,8) + l1 = plt.hist(wobs,bins=bins,label="Observed",alpha=0.5) + + # makes a function of completeness + wxvals,wyvals = make_completeness_plot(maxws) + + wi_comp = get_completeness(wobs,wxvals,wyvals) + + l2 = plt.hist(wobs,bins=bins,weights = 1./wi_comp,label="Observed",alpha=0.5) + + + ax2 = ax1.twinx() + l3 = ax2.plot(wxvals,wyvals,label="Completeness") + plt.ylim(0,1) + plt.xlim(5e-3,1e2) + plt.ylabel("Completeness") + plt.sca(ax1) + plt.legend(handles=[l1[2],l3[0],l2[2]],labels=["Observed","Completeness","Corrected $w_i$"],fontsize=12) + plt.xlabel("$w_i$ [ms]") + plt.ylabel("Number of FRBs") + + plt.text(1e-3,8,"(a)",fontsize=18) + plt.tight_layout() + plt.savefig(outdir+"w_observed_histogram.png") + plt.close() + + ################################ 1 GHz Rest-frame Histogram ########################## + + plt.figure() + ax1=plt.gca() + plt.xscale("log") + plt.ylim(0,8) + l1 = plt.hist(host_w,bins=bins,label="Host",alpha=0.5) + + # makes a function of completeness + wxvals,wyvals = make_completeness_plot(host_maxw) + + # get completeness at points of measurement + w_comp = get_completeness(host_w,wxvals,wyvals) + + l2 = plt.hist(host_w,bins=bins,weights = 1./w_comp,label="Observed",alpha=0.5) + + + ax2 = ax1.twinx() + l3 = ax2.plot(wxvals,wyvals,label="Completeness") + + plt.ylim(0,1) + plt.xlim(5e-3,1e2) + plt.ylabel("Completeness") + plt.sca(ax1) + plt.legend(handles=[l1[2],l3[0],l2[2]],labels=["Observed","Completeness","Corrected"],fontsize=12) + plt.xlabel("$w_{i,\\rm host}$ [ms]") + plt.ylabel("Number of FRBs") + plt.tight_layout() + plt.savefig(outdir+"w_host_histogram.png") + + # keeps open for plotting later + + #### new plot, for paper - just a copy of the above #### + + #"everything" + plt.figure() + plt.xscale("log") + plt.ylim(0,8) + l1v2 = plt.hist(host_w,bins=bins,label="Host",alpha=0.5) + + ax1v2=plt.gca() + # makes a function of completeness + wxvals,wyvals = make_completeness_plot(host_maxw) + + # get completeness at points of measurement + w_comp = get_completeness(host_w,wxvals,wyvals) + + l2v2 = plt.hist(host_w,bins=bins,weights = 1./w_comp,label="Observed",alpha=0.5) + + ax2v2 = ax1v2.twinx() + l3v2 = ax2v2.plot(wxvals,wyvals,label="Completeness")#,color=use_this_color) + + plt.ylim(0,1) + plt.ylabel("Completeness") + plt.sca(ax1v2) + plt.legend(handles=[l1v2[2],l3v2[0],l2v2[2]],labels=["Observed","Completeness","Corrected"],fontsize=12) + plt.xlabel("$w_{i,\\rm host}$ [ms]") + plt.ylabel("Number of FRBs") + + + # "observed" figures # + plt.figure() + #obshist,bins = np.histogram(tauobs,bins=bins) + #hosthist,bins = np.histogram(host_tau,bins=bins) + ax1v3=plt.gca() + plt.xscale("log") + plt.ylim(0,11) + l1v3 = plt.hist(host_w,bins=bins,label="Observed",alpha=0.5) + + ax2v3 = ax1v3.twinx() + l3v3 = ax2v3.plot(wxvals,wyvals,label="Completeness") + + plt.ylim(0,1) + plt.ylabel("Completeness") + plt.sca(ax1) + plt.xlabel("$w_{i,\\rm host}$ [ms]") + plt.ylabel("Number of FRBs") + # keeps open for later plotting - don't close this here + + # "intrinsic" figures # + plt.figure() + #obshist,bins = np.histogram(tauobs,bins=bins) + #hosthist,bins = np.histogram(host_tau,bins=bins) + ax1v4=plt.gca() + plt.xscale("log") + plt.ylim(0,11) + l2v4 = plt.hist(host_w,bins=bins,weights = 1./w_comp,label="Corrected",alpha=0.5) + + + ax2v4 = ax1v4.twinx() + l3v4 = ax2v4.plot(wxvals,wyvals,label="Completeness") + + plt.ylim(0,1) + plt.ylabel("Completeness") + plt.sca(ax1) + plt.xlabel("$w_{\\rm host}$ [ms]") + plt.ylabel("Intrinsic number of FRBs") + # keeps open for later plotting - don't close this here - ax1v2=plt.gca() + ####################### W - likelihood maximisation ################# # makes a function of completeness wxvals,wyvals = make_completeness_plot(host_maxw) - - # get completeness at points of measurement - w_comp = get_completeness(host_w,wxvals,wyvals) - - l2v2 = plt.hist(host_w,bins=bins,weights = 1./w_comp,label="Observed",alpha=0.5) - - ax2v2 = ax1v2.twinx() - l3v2 = ax2v2.plot(wxvals,wyvals,label="Completeness")#,color=use_this_color) - - plt.ylim(0,1) - plt.ylabel("Completeness") - plt.sca(ax1v2) - plt.legend(handles=[l1v2[2],l3v2[0],l2v2[2]],labels=["Observed","Completeness","Corrected"],fontsize=12) - plt.xlabel("$w_{i,\\rm host}$ [ms]") - plt.ylabel("Number of FRBs") - - - # "observed" figures # - plt.figure() - #obshist,bins = np.histogram(tauobs,bins=bins) - #hosthist,bins = np.histogram(host_tau,bins=bins) - ax1v3=plt.gca() - plt.xscale("log") - plt.ylim(0,11) - l1v3 = plt.hist(host_w,bins=bins,label="Observed",alpha=0.5) - - ax2v3 = ax1v3.twinx() - l3v3 = ax2v3.plot(wxvals,wyvals,label="Completeness") - - plt.ylim(0,1) - plt.ylabel("Completeness") - plt.sca(ax1) - plt.xlabel("$w_{i,\\rm host}$ [ms]") - plt.ylabel("Number of FRBs") - # keeps open for later plotting - don't close this here - - # "intrinsic" figures # - plt.figure() - #obshist,bins = np.histogram(tauobs,bins=bins) - #hosthist,bins = np.histogram(host_tau,bins=bins) - ax1v4=plt.gca() - plt.xscale("log") - plt.ylim(0,11) - l2v4 = plt.hist(host_w,bins=bins,weights = 1./w_comp,label="Corrected",alpha=0.5) - - - ax2v4 = ax1v4.twinx() - l3v4 = ax2v4.plot(wxvals,wyvals,label="Completeness") - - plt.ylim(0,1) - plt.ylabel("Completeness") - plt.sca(ax1) - plt.xlabel("$w_{\\rm host}$ [ms]") - plt.ylabel("Intrinsic number of FRBs") - # keeps open for later plotting - don't close this here - - ####################### W - likelihood maximisation ################# - + ####################################### Width - CDF and fitting ################################ - print("\n\n KS test evaluation for width \n") + if doplots: + print("\n\n KS test evaluation for width \n") # amplitude, mean, and std dev of true distribution ksbest = [] @@ -744,11 +862,13 @@ def main(outdir="Fitting_Outputs/"): result = sp.optimize.minimize(get_ks_stat,x0=x0,args=args,method = 'Nelder-Mead') psub1 = get_ks_stat(result.x,*args,plot=False) ksbest.append(result.x) - print("FUNCTION ",ifunc,",",FNAMES[ifunc]," Best-fitting parameters are ",result.x," with p-value ",-result.fun) + if doplots: + print("FUNCTION ",ifunc,",",FNAMES[ifunc]," Best-fitting parameters are ",result.x," with p-value ",-result.fun) - print("\n\n Maximum likelihood for width \n") + if doplots: + print("\n\n Maximum likelihood for width \n") ### makes temporary values for completeness xtemp = wxvals[::2] ytemp = wyvals[::2] @@ -757,16 +877,16 @@ def main(outdir="Fitting_Outputs/"): yt = np.zeros([s+2]) xt[0] = xtemp[0] yt[0] = ytemp[0] - xt[1] = 0.1 - yt[1] = 1. xt[2:-1] = xtemp[1:] yt[2:-1] = ytemp[1:] + xt[1] = xt[2]*0.75 + yt[1] = 1. xt[-1] = 1e5 yt[-1] = 0. cspline = sp.interpolate.make_interp_spline(np.log10(xt), yt,k=1) # do a test plot of the spline? - if True: + if doplots: plt.figure() plt.plot(np.logspace(-5,5,101),cspline(np.linspace(-5,5,101))) plt.plot(xvals,yvals) @@ -774,14 +894,17 @@ def main(outdir="Fitting_Outputs/"): plt.savefig(outdir+"width_spline_example.png") plt.close() # make a cdf plot of the best fits - make_cdf_plot(ksbest,host_tau,xvals,yvals,outdir+"bestfit_ks_width_cumulative.png",cspline) + if doplots: + make_cdf_plot(ksbest,host_tau,xvals,yvals,outdir+"bestfit_ks_width_cumulative.png",cspline) ####################################### Width - max likelihood ################################ - print("\n\n Likelhiood maximasation for width \n") + if doplots: + print("\n\n Likelhiood maximasation for width \n") # amplitude, mean, and std dev of true distribution xbest=[] llbests = [] + pvals = [] # iterate over functions to calculate max likelihood for ifunc in np.arange(NFUNC): @@ -792,136 +915,138 @@ def main(outdir="Fitting_Outputs/"): llbests.append(llbest) #psub1 = get_ks_stat(result.x,*args,plot=True) xbest.append(result.x) - print("width FUNCTION ",ifunc,",",FNAMES[ifunc]," Best-fitting log-likelihood parameters are ",result.x," with p-value ",1.-result.fun) + pvals.append(-result.fun) + if doplots: + print("width FUNCTION ",ifunc,",",FNAMES[ifunc]," Best-fitting log-likelihood parameters are ",result.x," with p-value ",-result.fun) if ifunc == 0: - llCHIME = get_ll_stat([CHIME_muw,CHIME_sw],host_tau,cspline,ifunc) * -1 - print("Compare with CHIME ",llCHIME) - - - print("\n\nLATEX TABLE") - for ifunc in np.arange(NFUNC): - string = FNAMES[ifunc] + " & $" + latexw[ifunc] + f"$ & ${xbest[ifunc][0]:.2f}" - for iarg, arg in enumerate(xbest[ifunc]): - if iarg==0: - continue - string += f", {xbest[ifunc][iarg]:.2f}" - string += f" $ & {llbests[ifunc]:.2f} \\\\" - print(string) - print("\n\n\n\n") - - - - make_cdf_plot(xbest,host_w,wxvals,wyvals,outdir+"bestfit_ll_width_cumulative.png",cspline,width=True) + wllCHIME = get_ll_stat([CHIME_muw,CHIME_sw],host_tau,cspline,ifunc) * -1 + if doplots: + print("Compare with CHIME ",wllCHIME) + wllCHIME -= llbests[3] + if doplots: + print("\n\nLATEX TABLE") + for ifunc in np.arange(NFUNC): + string = FNAMES[ifunc] + " & $" + latexw[ifunc] + f"$ & ${xbest[ifunc][0]:.2f}" + for iarg, arg in enumerate(xbest[ifunc]): + if iarg==0: + continue + string += f", {xbest[ifunc][iarg]:.2f}" + string += f" $ & {llbests[ifunc]:.2f} \\\\" + print(string) + print("\n\n\n\n") + + if doplots: + make_cdf_plot(xbest,host_w,wxvals,wyvals,outdir+"bestfit_ll_width_cumulative.png",cspline,width=True) ### does plot with fits added ### - - plt.sca(ax1) - NFRB=host_tau.size - handles=[l1[2],l3[0],l2[2]] - labels=["$w_{\\rm host}$","Completeness","Corrected $w_{\\rm host}$"] - for i in np.arange(NFUNC): - print("plotting function ",i," with xbest ",xbest[i]) - xs,ys = function_wrapper(i,xbest[i])#cspline=None): - plotnorm = NFRB * (np.log10(bins[1])-np.log10(bins[0])) - l=plt.plot(xs,ys*plotnorm,label=FNAMES[i]) - handles.append(l[0]) - labels.append(FNAMES[i]) - xs,ys = function_wrapper(i,xbest[i],cspline=cspline) - plt.plot(xs,ys*plotnorm,linestyle=":",color=plt.gca().lines[-1].get_color()) - plt.xscale("log") - plt.xlim(1e-4,1e3) - plt.legend() - - plt.legend(handles=handles,labels=labels,fontsize=6) #fontsize=12) - plt.savefig(outdir+"w_host_histogram_fits.png") - plt.close() - - #### for paper #### - - - plt.sca(ax1v2) - plt.ylim(0,12) - NFRB=host_w.size - handles=[l1v2[2],l3v2[0],l2v2[2]] - labels=["Observed","Completeness","Corrected"] - for i in [0,1,3]: - print("plotting function ",i," with xbest ",xbest[i]) - xs,ys = function_wrapper(i,xbest[i])#cspline=None): - plotnorm = NFRB * (np.log10(bins[1])-np.log10(bins[0])) - l=plt.plot(xs,ys*plotnorm,label=FNAMES[i]) - handles.append(l[0]) - labels.append(FNAMES[i]) - xs,ys = function_wrapper(i,xbest[i],cspline=cspline) - plt.plot(xs,ys*plotnorm,linestyle=":",color=plt.gca().lines[-1].get_color()) - plt.xscale("log") - - plt.text(1e-3,12,"(b)",fontsize=18) - plt.xlim(5e-3,1e2) - plt.legend() - plt.xlabel("$w_{\\rm host}$ [ms]") - plt.ylabel("Number of FRBs") - plt.legend(handles=handles,labels=labels,fontsize=12) #fontsize=12) - - plt.tight_layout() - plt.savefig(outdir+"paper_w_host_histogram_fits.png") - plt.close() - - - #"observed" - plt.sca(ax1v3) - NFRB=host_tau.size - handles=[l1v3[2],l3v3[0]] - labels=["Observed","Completeness"] - for i in [0,1,3]: - print("plotting function ",i," with xbest ",xbest[i]) - xs,ys = function_wrapper(i,xbest[i],logxmax=2)#cspline=None): - plotnorm = NFRB * (np.log10(bins[1])-np.log10(bins[0])) - #l=plt.plot(xs,ys*plotnorm,label=FNAMES[i]) - - xs,ys = function_wrapper(i,xbest[i],cspline=cspline) - l=plt.plot(xs,ys*plotnorm,label=FNAMES[i],linestyle=styles[i]) - handles.append(l[0]) - labels.append(FNAMES[i]) - - plt.xscale("log") - plt.xlim(5e-3,1e2) - - plt.text(1e-3,10.5,"(a)",fontsize=18) - plt.legend() - plt.xlabel("$w_{\\rm host}$ [ms]") - plt.ylabel("Observed number of FRBs") - plt.legend(handles=handles,labels=labels,fontsize=10) #fontsize=12) - - plt.tight_layout() - plt.savefig(outdir+"observed_paper_width_host_histogram_fits.png") - plt.close() - - # "intrinsic" - plt.sca(ax1v4) - NFRB=host_tau.size - handles=[l2v4[2],l3v4[0]] - labels=["Adjusted","Completeness"] - - for i in [0,1,3]: - print("plotting function ",i," with xbest ",xbest[i]) - xs,ys = function_wrapper(i,xbest[i],logxmax=2)#cspline=None): - plotnorm = NFRB * (np.log10(bins[1])-np.log10(bins[0])) - l=plt.plot(xs,ys*plotnorm,label=FNAMES[i],linestyle=styles[i]) - handles.append(l[0]) - labels.append(FNAMES[i]) - plt.xscale("log") - plt.xlim(5e-3,1e2) - - plt.text(1e-3,10.5,"(b)",fontsize=18) - plt.legend() - plt.xlabel("$w_{\\rm host}$ [ms]") - plt.ylabel("Intrinsic number of FRBs") - plt.legend(handles=handles,labels=labels,fontsize=10) #fontsize=12) - - plt.tight_layout() - plt.savefig(outdir+"intrinsic_paper_width_host_histogram_fits.png") - plt.close() + if doplots: + plt.sca(ax1) + NFRB=host_tau.size + handles=[l1[2],l3[0],l2[2]] + labels=["$w_{\\rm host}$","Completeness","Corrected $w_{\\rm host}$"] + for i in np.arange(NFUNC): + print("plotting function ",i," with xbest ",xbest[i]) + xs,ys = function_wrapper(i,xbest[i])#cspline=None): + plotnorm = NFRB * (np.log10(bins[1])-np.log10(bins[0])) + l=plt.plot(xs,ys*plotnorm,label=FNAMES[i]) + handles.append(l[0]) + labels.append(FNAMES[i]) + xs,ys = function_wrapper(i,xbest[i],cspline=cspline) + plt.plot(xs,ys*plotnorm,linestyle=":",color=plt.gca().lines[-1].get_color()) + plt.xscale("log") + plt.xlim(1e-4,1e3) + plt.legend() + + plt.legend(handles=handles,labels=labels,fontsize=6) #fontsize=12) + plt.savefig(outdir+"w_host_histogram_fits.png") + plt.close() + + #### for paper #### + + + plt.sca(ax1v2) + plt.ylim(0,12) + NFRB=host_w.size + handles=[l1v2[2],l3v2[0],l2v2[2]] + labels=["Observed","Completeness","Corrected"] + for i in [0,1,3]: + print("plotting function ",i," with xbest ",xbest[i]) + xs,ys = function_wrapper(i,xbest[i])#cspline=None): + plotnorm = NFRB * (np.log10(bins[1])-np.log10(bins[0])) + l=plt.plot(xs,ys*plotnorm,label=FNAMES[i]) + handles.append(l[0]) + labels.append(FNAMES[i]) + xs,ys = function_wrapper(i,xbest[i],cspline=cspline) + plt.plot(xs,ys*plotnorm,linestyle=":",color=plt.gca().lines[-1].get_color()) + plt.xscale("log") + + plt.text(1e-3,12,"(b)",fontsize=18) + plt.xlim(5e-3,1e2) + plt.legend() + plt.xlabel("$w_{\\rm host}$ [ms]") + plt.ylabel("Number of FRBs") + plt.legend(handles=handles,labels=labels,fontsize=12) #fontsize=12) + + plt.tight_layout() + plt.savefig(outdir+"paper_w_host_histogram_fits.png") + plt.close() + + + #"observed" + plt.sca(ax1v3) + NFRB=host_tau.size + handles=[l1v3[2],l3v3[0]] + labels=["Observed","Completeness"] + for i in [0,1,3]: + print("plotting function ",i," with xbest ",xbest[i]) + xs,ys = function_wrapper(i,xbest[i],logxmax=2)#cspline=None): + plotnorm = NFRB * (np.log10(bins[1])-np.log10(bins[0])) + #l=plt.plot(xs,ys*plotnorm,label=FNAMES[i]) + + xs,ys = function_wrapper(i,xbest[i],cspline=cspline) + l=plt.plot(xs,ys*plotnorm,label=FNAMES[i],linestyle=styles[i]) + handles.append(l[0]) + labels.append(FNAMES[i]) + + plt.xscale("log") + plt.xlim(5e-3,1e2) + + plt.text(1e-3,10.5,"(a)",fontsize=18) + plt.legend() + plt.xlabel("$w_{\\rm host}$ [ms]") + plt.ylabel("Observed number of FRBs") + plt.legend(handles=handles,labels=labels,fontsize=10) #fontsize=12) + + plt.tight_layout() + plt.savefig(outdir+"observed_paper_width_host_histogram_fits.png") + plt.close() + + # "intrinsic" + plt.sca(ax1v4) + NFRB=host_tau.size + handles=[l2v4[2],l3v4[0]] + labels=["Adjusted","Completeness"] + + for i in [0,1,3]: + print("plotting function ",i," with xbest ",xbest[i]) + xs,ys = function_wrapper(i,xbest[i],logxmax=2)#cspline=None): + plotnorm = NFRB * (np.log10(bins[1])-np.log10(bins[0])) + l=plt.plot(xs,ys*plotnorm,label=FNAMES[i],linestyle=styles[i]) + handles.append(l[0]) + labels.append(FNAMES[i]) + plt.xscale("log") + plt.xlim(5e-3,1e2) + + plt.text(1e-3,10.5,"(b)",fontsize=18) + plt.legend() + plt.xlabel("$w_{\\rm host}$ [ms]") + plt.ylabel("Intrinsic number of FRBs") + plt.legend(handles=handles,labels=labels,fontsize=10) #fontsize=12) + + plt.tight_layout() + plt.savefig(outdir+"intrinsic_paper_width_host_histogram_fits.png") + plt.close() ############################################# WIDTH - bayes factor ####################################### @@ -932,10 +1057,12 @@ def main(outdir="Fitting_Outputs/"): # min mean/min/max: size of distribution # NOTE: Bayes factor is P(data|model1)/P(data|model2) - print("\n\n Bayes Factor Calculation\n") + if doplots: + print("\n\n Bayes Factor Calculation\n") for ifunc in np.arange(NFUNC): if True: - print("skipping Bayes factor calculation for width, remove this line to re-run") + if doplots: + print("skipping Bayes factor calculation for width, remove this line to re-run") #FUNCTION 0 , lognormal has likelihood sum 4.287511548315901e-15 now compute Bayes factor! #FUNCTION 1 , half-lognormal has likelihood sum 1.3919340351669428e-12 now compute Bayes factor! #FUNCTION 2 , boxcar has likelihood sum 3.1091474793575766e-13 now compute Bayes factor! @@ -943,7 +1070,7 @@ def main(outdir="Fitting_Outputs/"): #FUNCTION 4 , smooth boxcar has likelihood sum 8.176332379444694e-13 now compute Bayes factor! #FUNCTION 5 , upper sb has likelihood sum 3.595417631395158e-13 now compute Bayes factor! #FUNCTION 6 , lower sb has likelihood sum 6.806166849685634e-13 now compute Bayes factor! - continue + break llsum=0. N1=100 @@ -980,9 +1107,11 @@ def main(outdir="Fitting_Outputs/"): if (ifunc == 2 or ifunc == 4 or ifunc==5 or ifunc==6): llsum *= 2 #because the parameter space is actually half that calculated above - print("FUNCTION ",ifunc,",",FNAMES[ifunc]," has likelihood sum ",llsum, " now compute Bayes factor!") - + if doplots: + print("FUNCTION ",ifunc,",",FNAMES[ifunc]," has likelihood sum ",llsum, " now compute Bayes factor!") + # returns found values + return tauxbest, taullbests, taupbests, xbest, llbests, pvals,taullCHIME,wllCHIME def plot_functions(outdir=""): """ @@ -1029,9 +1158,6 @@ def plot_functions(outdir=""): plt.plot(xs,7+ys/np.max(ys)-1.1*ifunc,label=FNAMES[ifunc],linestyle=styles[ifunc%4]) plt.text(xlabels[ifunc],7.5-1.1*ifunc,FNAMES[ifunc],color=plt.gca().lines[-1].get_color()) - - - plt.xlim(1e-4,1e4) plt.xlabel("t [ms]") plt.xscale("log") @@ -1406,25 +1532,32 @@ def get_data(): ERR=9999. tns = dataframe.TNS tauobs = dataframe.TauObs + tauobserr = dataframe.TauObsErr w95 = dataframe.W95 wsnr = dataframe.Wsnr z = dataframe.Z snr = dataframe.SNdet freq = dataframe.NUTau DM = dataframe.DM + alpha = dataframe.TauAlpha + alphaerr = dataframe.TauAlphaErr - # check which FRBs do not have nay errors in all columns + # check which FRBs do not have any errors in all columns OK = getOK([tns,tauobs,w95,wsnr,z,snr,freq,DM]) tns = tns[OK] tauobs= tauobs[OK] + tauobserr = tauobserr[OK] w95 = w95[OK] wsnr = wsnr[OK] z = z[OK] snr = snr[OK] freq = freq[OK] DM = DM[OK] + alpha = alpha[OK] + alphaerr = alphaerr[OK] NFRB = len(OK) + tres = np.zeros([NFRB]) for i,name in enumerate(tns): j = np.where(name[0:8] == names)[0] @@ -1445,8 +1578,11 @@ def get_data(): freq = np.array(freq) DM = np.array(DM) tres = np.array(tres) + tauobserr = np.array(tauobserr) + alpha = np.array(alpha) + alphaerr = np.array(alphaerr) - return tns,tauobs,w95,wsnr,z,snr,freq,DM,tres + return tns,tauobs,w95,wsnr,z,snr,freq,DM,tres,tauobserr,alpha,alphaerr @@ -1496,4 +1632,172 @@ def make_cum_dist(vals): ys[-1] = 1 return xs,ys -main() + +# actual best fits +truetauxbest, truetaullbests, truetaupbests, truewxbest, truewllbests, truewpvalst,taullCHIME,wllCHIME = fit_scat_width(bootstrap=False,doplots=True,alpha=-4) + +######## resused bootstrap code for alpha ############# + + +NALPHA=11 +alphas = np.linspace(-4,0,NALPHA) +NPARAMS=3 # max params in any given model +taufitresults = np.zeros([NALPHA,NFUNC,NPARAMS]) +wfitresults = np.zeros([NALPHA,NFUNC,NPARAMS]) +taullresults = np.zeros([NALPHA,NFUNC]) +wllresults = np.zeros([NALPHA,NFUNC]) + + +for i,alpha in enumerate(alphas): + # max likelihood fitting results for all models + tauxbest, taullbests, taupbests, wxbest, wllbests, wpvals,taullCHIME,wllCHIME = fit_scat_width(alpha=alpha) + + print("ALPHA is ",alpha," relative log likelihoods are ",taullCHIME,wllCHIME) + + for j,res in enumerate(tauxbest): + length = len(res) + taufitresults[i,j,:length]=res + + for j,res in enumerate(wxbest): + length = len(res) + wfitresults[i,j,:length]=res + + taullresults[i,:] = taullbests + wllresults[i,:] = wllbests + +plt.figure() +plt.xlabel("$\\alpha$") +plt.ylabel("$\\log_{10} {L} - \\log_{10} {L_{\\rm lognormal}}$") +#doit = np.arange(NFUNC) +doit = [1,2,3] +styles = ["-","--","-.",":","-","--","-.",":","-","--","-.",":"] +ax1 = plt.gca() +ax2 = ax1.twinx() + + +for i,j in enumerate(doit): + string="" + ax1.plot(alphas,taullresults[:,j]-taullresults[:,0],label=FNAMES[j],linestyle=styles[i]) + #plt.plot(alphas,wllresults[:,j]-wllresults[:,0],label=FNAMES[j],linestyle=styles[i]) + +ax2.plot(alphas,taufitresults[:,0,0],label="ASKAP $\mu_{\\tau, 1\,GHz}$",color="black",linestyle="-") +ax2.plot(alphas,CHIME_mut + np.log10((1000/600)**alphas),label="CHIME $\\mu_{\\tau, 1\,GHz}$",color="black",linestyle="--") + +#ax2.plot(alphas,taufitresults[:,0,1],label="ASKAP $\sigma_{\\tau, 1\,GHz}$",color="gray",linestyle="-.") +#ax2.plot([alphas[0],alphas[-1]],[CHIME_st,CHIME_st],label="CHIME $\\sigma_{\\tau, 1\,GHz}$",color="gray",linestyle=":") +plt.sca(ax2) +plt.ylabel("Parameter values") +#plt.legend(loc="upper right") +plt.legend() +plt.sca(ax1) + +#plt.legend(loc="lower left") +plt.legend() +plt.tight_layout() +plt.savefig("alpha_variation_results.png") +#plt.savefig("width_alpha_variation_results.png") +plt.close() + + +######### does this for bootstrapping scattering ###### + +NBOOTSTRAP=100 +NPARAMS=3 # max params in any given model +taufitresults = np.zeros([NBOOTSTRAP,NFUNC,NPARAMS]) +wfitresults = np.zeros([NBOOTSTRAP,NFUNC,NPARAMS]) +taullresults = np.zeros([NBOOTSTRAP,NFUNC]) +wllresults = np.zeros([NBOOTSTRAP,NFUNC]) + + +for i in np.arange(NBOOTSTRAP): + # max likelihood fitting results for all models + print("Doing bootstrap ",i) + tauxbest, taullbests, taupbests, wxbest, wllbests, wpvals,taullCHIME,wllCHIME = fit_scat_width(bootstrap=True,bsalpha=False) + + for j,res in enumerate(tauxbest): + length = len(res) + taufitresults[i,j,:length]=res + + for j,res in enumerate(wxbest): + length = len(res) + wfitresults[i,j,:length]=res + + taullresults[i,:] = taullbests + wllresults[i,:] = wllbests + +plt.figure() +plt.xlabel("run") +plt.ylabel("param value") +print("Parameters for tau") +for j in np.arange(NFUNC): + string="" + for k,val in enumerate(truetauxbest[j]): + plt.plot(taufitresults[:,j,k],label=FNAMES[j]+" param "+str(k)) + mean = np.sum(taufitresults[:,j,k])/NBOOTSTRAP + rms = (np.sum((taufitresults[:,j,k] - mean)**2)/(NBOOTSTRAP-1))**0.5 + print(j,k,"Orig mean ",val," this mean ",mean," std err ",rms) + print("\n") +plt.legend(fontsize=4) +plt.tight_layout() +plt.savefig("tau_bootstrap_results.png") +plt.close() + + +plt.figure() +plt.xlabel("run") +plt.ylabel("param value") +print("\n\n\n\nParameters for width") +for j in np.arange(NFUNC): + string="" + for k,val in enumerate(truewxbest[j]): + plt.plot(wfitresults[:,j,k],label=FNAMES[j]+" param "+str(k)) + mean = np.sum(wfitresults[:,j,k])/NBOOTSTRAP + rms = (np.sum((wfitresults[:,j,k] - mean)**2)/(NBOOTSTRAP-1))**0.5 + print(j,k,"Orig mean ",val," this mean ",mean," std err ",rms) + print("\n") +plt.legend(fontsize=4) +plt.tight_layout() +plt.savefig("width_bootstrap_results.png") +plt.close() + + +# generates a distribution in difference of log-likelihood values for each model compared to lognormal +#Scattering + +for i in np.arange(NFUNC): + + print("ll for function ",FNAMES[i]) + #removes nans + OK = np.where(np.isfinite(taullresults[:,i]) == True)[0] + NOK = len(OK) + mean = np.mean(taullresults[OK,i]) + rms = (np.sum((taullresults[OK,i]-mean)**2)/(NOK-1))**0.5 + print("For scattering, mean and rms in likelihood is ",mean,rms) + + OK = np.where(np.isfinite(wllresults[:,i]) == True)[0] + NOK = len(OK) + mean = np.mean(wllresults[OK,i]) + rms = (np.sum((wllresults[OK,i]-mean)**2)/(NOK-1))**0.5 + print("For width, mean and rms in likelihood is ",mean,rms) + print("\n") + +print("##### log likelihood differences #####") + +for i in np.arange(NFUNC): + if i==0: + continue + print("dll for function ",FNAMES[i]) + dtaullresults = taullresults[:,i] - taullresults[:,0] + OK = np.where(np.isfinite(dtaullresults) == True)[0] + NOK = len(OK) + mean = np.mean(dtaullresults[OK]) + rms = (np.sum((dtaullresults[OK]-mean)**2)/(NOK-1))**0.5 + print("For scattering, mean and rms in likelihood difference is ",mean,rms) + + dwllresults = wllresults[:,i] - wllresults[:,0] + OK = np.where(np.isfinite(dwllresults) == True)[0] + NOK = len(OK) + mean = np.mean(dwllresults[OK]) + rms = (np.sum((dwllresults[OK]-mean)**2)/(NOK-1))**0.5 + print("For width, mean and rms in likelihood difference is ",mean,rms) + print("\n") diff --git a/papers/lsst/Photometric/plot_2dgrids.py b/papers/lsst/Photometric/plot_2dgrids.py index 79426ab7..8fb4808f 100644 --- a/papers/lsst/Photometric/plot_2dgrids.py +++ b/papers/lsst/Photometric/plot_2dgrids.py @@ -28,10 +28,10 @@ def main(): state=states.load_state(case="HoffmannHalo25",scat=None,rep=None) sdir = resources.files('zdm').joinpath('../papers/lsst/Photometric') names = ["Spectroscopic","Smeared","zFrac","Smeared_and_zFrac"] - + xlabels = ["$z_{\\rm spec}$","$z_{\\rm photo}$","$z_{\\rm spec}$","$z_{\\rm photo}$"] ss,gs = loading.surveys_and_grids( survey_names=names,repeaters=False,init_state=state,sdir=sdir) - plot_grids(gs,ss,"./") + plot_grids(gs,ss,"./",xlabels) #============================================================================== @@ -47,8 +47,9 @@ def main(): outdir = output directory val = parameter value for this grid """ -def plot_grids(grids, surveys, outdir): - for g,s in zip(grids, surveys): +def plot_grids(grids, surveys, outdir,xlabels): + for i,g in enumerate(grids): + s = surveys[i] zvals=[] dmvals=[] nozlist=[] @@ -72,6 +73,7 @@ def plot_grids(grids, surveys, outdir): norm=3, log=True, label="$\\log_{10} p({\\rm DM}_{\\rm EG},z)$ [a.u.]", + xlabel=xlabels[i], project=False, FRBDMs=frbdmvals, FRBZs=frbzvals, diff --git a/papers/lsst/Photometric/plot_H0_slice.py b/papers/lsst/Photometric/plot_H0_slice.py index 6ec52b0a..d1b7d10b 100644 --- a/papers/lsst/Photometric/plot_H0_slice.py +++ b/papers/lsst/Photometric/plot_H0_slice.py @@ -42,7 +42,7 @@ def main(): # up to the user to get thius order right! Use e.g. # python run_H0_slice.py -n 10 --min=50 --max=100 -f Spectroscopic Smeared zFrac Smeared_and_zFrac - s_names=["All hosts, spec-zs","$\\sigma_z=0.03$","$m_r^{\\rm lim}=24.7$","$\\sigma_z=0.03,~m_r^{\\rm lim}=24.7$"] + s_names=["All hosts, spec-zs","$\\sigma_z=0.035$","$m_r^{\\rm lim}=24.7$","$\\sigma_z=0.035,~m_r^{\\rm lim}=24.7$"] plt.clf() llsum = np.zeros(ll_lists.shape[0]) FWHM=[] diff --git a/papers/lsst/sim_pz.py b/papers/lsst/sim_pz.py index 0c2edc08..8a148a37 100644 --- a/papers/lsst/sim_pz.py +++ b/papers/lsst/sim_pz.py @@ -48,7 +48,7 @@ def main(): meerkat_z,meerkat_mr,meerkat_w = read_meerkat() # we should re-do this shortly. - Load=False + Load=True repeaters=False Test=False # do this for very simplified data Scat=False # do not use updated scattering model @@ -138,7 +138,7 @@ def main(): plt.ylabel("fraction visible") plt.plot(zvals,fz1,label="$m_{r}^{\\rm lim}=24.7$") plt.plot(zvals,fz2,label="$m_{r}^{\\rm lim}=27.5$",linestyle="--") - plt.ylim(0,1) + plt.ylim(0,1.05) plt.xlim(0,6) plt.legend() plt.tight_layout() @@ -205,8 +205,8 @@ def main(): plt.plot(zvals,pz/norm,label=labels[i],linestyle="-") #plt.plot(zvals,pz0/norm,linestyle="-.",color=plt.gca().lines[-1].get_color()) - plt.plot(zvals,pz1/norm,linestyle="--",color=plt.gca().lines[-1].get_color()) - plt.plot(zvals,pz2/norm,linestyle=":",color=plt.gca().lines[-1].get_color()) + plt.plot(zvals,pz1/norm,linestyle="--",color=plt.gca().lines[-1].get_color(),label=' single visit') + plt.plot(zvals,pz2/norm,linestyle=":",color=plt.gca().lines[-1].get_color(),label=' 10 yr co-adds') print("For survey ",prefixes[i]," number of FRBs will be ",np.sum(pz),np.sum(pz0),np.sum(pz1),\ np.sum(pz2),np.sum(pz0)/np.sum(pz),np.sum(pz1)/np.sum(pz),np.sum(pz2)/np.sum(pz)) @@ -230,7 +230,7 @@ def main(): plt.sca(ax1) plt.ylim(0,1.02) plt.xlim(0,5) - plt.legend() + plt.legend(fontsize=16) plt.tight_layout() plt.savefig(plotdir+"lsst_pz.png") plt.close() diff --git a/zdm/figures.py b/zdm/figures.py index 8dc48792..24119deb 100644 --- a/zdm/figures.py +++ b/zdm/figures.py @@ -30,6 +30,7 @@ def plot_grid( log=True, name="temp.pdf", label='$\\log_{10}p(DM_{\\rm EG},z)$', + xlabel="z", ylabel="${\\rm DM}_{\\rm EG}$ (pc cm$^{-3}$)", logrange=4, project=False, @@ -73,6 +74,7 @@ def plot_grid( log (bool, optional): Plot P(z,DM) in log space name (str, optional): Outfile name label (str, optional): Colourbar label + xlabel (str,optional): Label on x axis of plot ylabel (str,optional): Label on y axis of plot logrange(float,optional): range in logspace of the z axis (defaults to 4) project (bool, optional): Add projections of P(z) and P(DM) @@ -183,7 +185,7 @@ def plot_grid( plt.sca(ax1) - plt.xlabel("z") + plt.xlabel(xlabel) plt.ylabel(ylabel) nz, ndm = zDMgrid.shape From fc983947fbd39d6b8e5feb2d306abd1f96356dec Mon Sep 17 00:00:00 2001 From: Clancy James Date: Tue, 27 Jan 2026 08:54:53 +0800 Subject: [PATCH 13/35] Updated to optical PATH priors, and minor updates to LSST plots for that paper --- .../CASATTA MFAA SKA2 FRB estimates.csv | 37 +++ papers/Casatta/README.txt | 3 + papers/Casatta/casatta_base.ecsv | 14 ++ papers/Casatta/plot_casatta.py | 105 ++++++++ papers/Casatta/sim_casatta.py | 149 +++++++++++ papers/lsst/Data/dsa_hosts.csv | 9 + papers/lsst/make_host_z_mag_plot.py | 11 +- papers/lsst/sim_pz.py | 2 +- .../pu_mr_vs_mag_CRAFT_VLT_FORS2_r.csv | 28 +++ papers/pathpriors/pU_g_mr/test_pogmr.py | 65 +++++ papers/pathpriors/plot_craft_optical_data.py | 143 +++++++++++ zdm/optical.py | 235 +++++++++++------- zdm/optical_numerics.py | 218 ++++++++++++++-- zdm/optical_params.py | 27 +- zdm/scripts/Path/optimise_host_priors.py | 31 ++- zdm/scripts/Path/plot_host_models.py | 45 ++-- zdm/states.py | 2 +- zdm/survey.py | 7 +- 18 files changed, 978 insertions(+), 153 deletions(-) create mode 100644 papers/Casatta/CASATTA MFAA SKA2 FRB estimates.csv create mode 100644 papers/Casatta/README.txt create mode 100644 papers/Casatta/casatta_base.ecsv create mode 100644 papers/Casatta/plot_casatta.py create mode 100644 papers/Casatta/sim_casatta.py create mode 100644 papers/pathpriors/pU_g_mr/pu_mr_vs_mag_CRAFT_VLT_FORS2_r.csv create mode 100644 papers/pathpriors/pU_g_mr/test_pogmr.py create mode 100644 papers/pathpriors/plot_craft_optical_data.py diff --git a/papers/Casatta/CASATTA MFAA SKA2 FRB estimates.csv b/papers/Casatta/CASATTA MFAA SKA2 FRB estimates.csv new file mode 100644 index 00000000..7fc0762b --- /dev/null +++ b/papers/Casatta/CASATTA MFAA SKA2 FRB estimates.csv @@ -0,0 +1,37 @@ +Array_name,SEFDJy,Tsys (K),Aeff (m^2),A/Tsys (m^2/K),FrequencyMHz,BandwidthMHz,FWHM_deg,Time_resolution_ms,Freq_res_MHz +AA-600MHz-1M-narrow,0.625,30,240000,8000,600,600,40,1,0.5 +AA-600MHz-256k-narrow,2.5,30,60000,2000,600,600,40,1,0.5 +AA-600MHz-64k-narrow,10,30,15000,500,600,600,40,1,0.5 +AA-600MHz-16k-narrow,40,30,3750,125,600,600,40,1,0.5 +AA-600MHz-4k-narrow,160,30,937.5,31.25,600,600,40,1,0.5 +AA-600MHz-1k-narrow,640,30,234.375,7.8125,600,600,40,1,0.5 +AA-600MHz-256-narrow,2560,30,58.59375,1.953125,600,600,40,1,0.5 +AA-600MHz-64-narrow,10240,30,14.6484375,0.48828125,600,600,40,1,0.5 +AA-600MHz-16-narrow,40960,30,3.662109375,0.1220703125,600,600,40,1,0.5 +AA-600MHz-1M-wide,0.625,30,240000,8000,600,600,120,1,0.5 +AA-600MHz-256k-wide,2.5,30,60000,2000,600,600,120,1,0.5 +AA-600MHz-64k-wide,10,30,15000,500,600,600,120,1,0.5 +AA-600MHz-16k-wide,40,30,3750,125,600,600,120,1,0.5 +AA-600MHz-4k-wide,160,30,937.5,31.25,600,600,120,1,0.5 +AA-600MHz-1k-wide,640,30,234.375,7.8125,600,600,120,1,0.5 +AA-600MHz-256-wide,2560,30,58.59375,1.953125,600,600,120,1,0.5 +AA-600MHz-64-wide,10240,30,14.6484375,0.48828125,600,600,120,1,0.5 +AA-600MHz-16-wide,40960,30,3.662109375,0.1220703125,600,600,120,1,0.5 +AA-1400MHz-1M-narrow,3.125,25,29600,1184,1400,1400,40,1,1 +AA-1400MHz-256k-narrow,12.5,25,7400,296,1400,1400,40,1,1 +AA-1400MHz-64k-narrow,50,25,1850,74,1400,1400,40,1,1 +AA-1400MHz-16k-narrow,200,25,462.5,18.5,1400,1400,40,1,1 +AA-1400MHz-4k-narrow,800,25,115.625,4.625,1400,1400,40,1,1 +AA-1400MHz-1k-narrow,3200,25,28.90625,1.15625,1400,1400,40,1,1 +AA-1400MHz-256-narrow,12800,25,7.2265625,0.2890625,1400,1400,40,1,1 +AA-1400MHz-64-narrow,51200,25,1.806640625,0.072265625,1400,1400,40,1,1 +AA-1400MHz-16-narrow,204800,25,0.4516601563,0.01806640625,1400,1400,40,1,1 +AA-1400MHz-1M-wide,3.125,25,29600,1184,1400,1400,120,1,1 +AA-1400MHz-256k-wide,12.5,25,7400,296,1400,1400,120,1,1 +AA-1400MHz-64k-wide,50,25,1850,74,1400,1400,120,1,1 +AA-1400MHz-16k-wide,200,25,462.5,18.5,1400,1400,120,1,1 +AA-1400MHz-4k-wide,800,25,115.625,4.625,1400,1400,120,1,1 +AA-1400MHz-1k-wide,3200,25,28.90625,1.15625,1400,1400,120,1,1 +AA-1400MHz-256-wide,12800,25,7.2265625,0.2890625,1400,1400,120,1,1 +AA-1400MHz-64-wide,51200,25,1.806640625,0.072265625,1400,1400,120,1,1 +AA-1400MHz-16-wide,204800,25,0.4516601563,0.01806640625,1400,1400,120,1,1 diff --git a/papers/Casatta/README.txt b/papers/Casatta/README.txt new file mode 100644 index 00000000..943617be --- /dev/null +++ b/papers/Casatta/README.txt @@ -0,0 +1,3 @@ +These calculations are temporary calcs for the ATNF ASKAP science day + +Data provided by Nithya and Josh diff --git a/papers/Casatta/casatta_base.ecsv b/papers/Casatta/casatta_base.ecsv new file mode 100644 index 00000000..729ceb01 --- /dev/null +++ b/papers/Casatta/casatta_base.ecsv @@ -0,0 +1,14 @@ +# %ECSV 1.0 +# --- +# datatype: +# - {name: TNS, datatype: string} +# - {name: DM, datatype: float64} +# - {name: DMG, datatype: float64} +# - {name: SNR, datatype: float64} +# - {name: SNRTHRESH, datatype: float64} +# meta: !!omap +# - {survey_data: "{\n \"observing\": {\n \"NORM_FRB\": 1,\n \"TOBS\": 1\n },\n \"telescope\": {\n \ +# \ \"DIAM\": 40,\n \"BMETHOD\": 0,\n \"NBEAMS\": 1,\n \"NBINS\": 10\n }\n}"} +# schema: astropy-2.0 +TNS DM DMG SNR SNRTHRESH +DUMMY 200 30 10. 10. diff --git a/papers/Casatta/plot_casatta.py b/papers/Casatta/plot_casatta.py new file mode 100644 index 00000000..9c0198e4 --- /dev/null +++ b/papers/Casatta/plot_casatta.py @@ -0,0 +1,105 @@ + +import numpy as np +from matplotlib import pyplot as plt + +import matplotlib + +defaultsize=14 +ds=4 +font = {'family' : 'Helvetica', + 'weight' : 'normal', + 'size' : defaultsize} +matplotlib.rc('font', **font) + + + +def main(): + """ + plots casatta simulation results + """ + df = read_casatta_params() + nsims,ncols = df.shape + + dailys = np.load("dailys.npy") + pzs = np.load("pzs.npy") + pdms = np.load("pdms.npy") + zvals = np.load("zvals.npy") + dmvals = np.load("dmvals.npy") + threshs = np.load("threshs.npy") + + # daily FRB rate for each config + plt.figure() + + plt.yscale('log') + plt.scatter(np.arange(nsims),dailys) + plt.xticks(np.arange(nsims),df["Array_name"],rotation=90,fontsize=6) + + plt.ylabel("Daily FRB rate") + plt.tight_layout() + plt.savefig("frb_rate_per_configuration.png") + plt.close() + + plt.figure() + plt.yscale("log") + + + # multiplies by z-bin width + dz = zvals[1]-zvals[0] + plt.xlabel("z") + plt.ylabel("p(z) [FRBs / day / z]") + plt.ylim(1e-3,1e5) + for isim in np.arange(nsims): + plt.plot(zvals,pzs[isim,:]/dz,label=df["Array_name"][isim]) + plt.legend(fontsize=4) + plt.tight_layout() + plt.savefig("all_pz.png") + plt.close() + + + plt.figure() + plt.yscale("log") + plt.ylim(1e-6,1e2) + # multiplies by DM width + ddm = dmvals[1]-dmvals[0] + + plt.xlabel("DM [pc cm$^{-3}$]") + plt.ylabel("p(DM) [FRBs /day /pc cm$^{-3}$]") + for isim in np.arange(nsims): + plt.plot(dmvals,pdms[isim,:]/ddm,label=df["Array_name"][isim]) + plt.legend(fontsize=4) + plt.tight_layout() + plt.savefig("all_pdm.png") + plt.close() + + + # compares estimates from nominal figure of merit + FOM = threshs**-1.5 * df["FWHM_deg"]**2 + + plt.figure() + plt.xlabel("FOM [FWHM$^2$ (Jy ms)$^{-1.5}$]") + plt.ylabel("Daily rate") + plt.xscale("log") + plt.yscale("log") + plt.scatter(dailys,FOM) + \ + plt.plot([1e-5,1e4],[0.05,5e7],color="black",label="1-1 line",linestyle="--") + plt.legend() + plt.tight_layout() + plt.savefig("FOM.png") + plt.close() + + +def read_casatta_params(infile="CASATTA MFAA SKA2 FRB estimates.csv"): + """ + Reads in casatta parameters + """ + + import pandas as pd + df = pd.read_csv(infile) + + return df + + +main() + + diff --git a/papers/Casatta/sim_casatta.py b/papers/Casatta/sim_casatta.py new file mode 100644 index 00000000..28fbeb06 --- /dev/null +++ b/papers/Casatta/sim_casatta.py @@ -0,0 +1,149 @@ + +import numpy as np +import importlib.resources as resources +import copy +import scipy.constants as constants +from matplotlib import pyplot as plt + +from zdm import states +from zdm import misc_functions as mf +from zdm import grid as zdm_grid +from zdm import survey +from zdm import pcosmic + +import matplotlib + +defaultsize=14 +ds=4 +font = {'family' : 'Helvetica', + 'weight' : 'normal', + 'size' : defaultsize} +matplotlib.rc('font', **font) + + + +def main(): + """ + main file to simulate casatta sensitivity + """ + df = read_casatta_params() + nsims,ncols = df.shape + + # state. Does not use updated scattering, because it takes a long time! + state = states.load_state("HoffmannHalo25")#scat="updated",rep=None) + + zDMgrid, zvals, dmvals = mf.get_zdm_grid( + state, new=True, plot=False, method='analytic', + datdir=resources.files('zdm').joinpath('GridData')) + + # we can keep this constant - it smears DM due to host DM + mask = pcosmic.get_dm_mask(dmvals, (state.host.lmean, state.host.lsigma), zvals, plot=False) + + sim = True + + threshs = np.zeros([nsims]) + dailys = np.zeros([nsims]) + pzs = np.zeros([nsims,zvals.size]) + pdms = np.zeros([nsims,dmvals.size]) + + for isim in np.arange(nsims): + daily,pz,pdm,thresh = sim_casatta(df.iloc[isim],state,zDMgrid, zvals, dmvals,mask) + dailys[isim]=daily + pzs[isim,:]=pz + pdms[isim,:]=pdm + threshs[isim] = thresh + print("Done simulation ",isim, "daily rate ",daily) + + np.save("threshs.npy",threshs) + np.save("dailys.npy",dailys) + np.save("pzs.npy",pzs) + np.save("pdms.npy",pdms) + np.save("zvals.npy",zvals) + np.save("dmvals.npy",dmvals) + + +def read_casatta_params(infile="CASATTA MFAA SKA2 FRB estimates.csv"): + """ + Reads in casatta parameters + """ + + import pandas as pd + df = pd.read_csv(infile) + + return df + +def sim_casatta(df,state,zDMgrid, zvals, dmvals, mask): + """ + simulates casatta for specific values given in dataframe + + Args: + df: dataframe containing info for this version of casatta + state: zdm state object + zDMgrid: underlying zDM grid giving p(DMcosmic|z) + zvals: redshift values of grid + dmvals: DM values of grid + mask: DM smearing mask for grid based on DMhost + """ + + # base name = casatta.ecsv + # directory where the base survey lives + sdir = resources.files('zdm').joinpath('../papers/Casatta/') + sdir = str(sdir) # convert to string, not annoying object + + ########### calculates relevant properties for CASATTA ####### + BW = df["BandwidthMHz"] + BWHz = BW*1e6 + tms = 1e-3 # seconds in one ms + + # SEFD in Jy. + NSAMP = 4.*BWHz *tms # 2 for polarisation, 2 for Nyquist, 1e3 for Jyms + nsigma=10 # S/N requirement for detection + THRESH = nsigma*df['SEFDJy']/NSAMP**0.5 + + FBAR = df["FrequencyMHz"] + tres = df["Time_resolution_ms"] + fres = df["Freq_res_MHz"] + + fMHz = df['FrequencyMHz'] + #print(df['Array_name'],df['SEFDJy'],NSAMP,THRESH) + + + # calculates what D to use + FWHM_rad = df["FWHM_deg"]*np.pi/180. + DIAM=1.22*(constants.c/(fMHz*1e6))/FWHM_rad + + # generates a survey dict to modify properties of this survey + # TOBS is one day + survey_dict = {"THRESH": THRESH, "TOBS": 1, "FBAR": float(fMHz), "BW": float(BW), "DIAM": DIAM, + "FRES": float(fres), "TRES": float(tres)} + + #uncomment this to calculate the rates for Fly's Eye + if False: + s = survey.load_survey("CRAFT_class_I_and_II", state, dmvals, zvals=zvals) + + g = zdm_grid.Grid(s, copy.deepcopy(state), zDMgrid, zvals, dmvals, mask, wdist=True) + + predicted = np.sum(g.rates)* s.TOBS * 10**state.FRBdemo.lC + expected = s.NORM_FRB + + print("expected ",expected,predicted) + + exit() + + + survey_name = "casatta_base" + s = survey.load_survey(survey_name, state, dmvals, zvals=zvals, survey_dict=survey_dict, sdir=sdir) + + g = zdm_grid.Grid(s, copy.deepcopy(state), zDMgrid, zvals, dmvals, mask, wdist=True) + + + daily = np.sum(g.rates)* 10**state.FRBdemo.lC + + pz = np.sum(g.rates,axis=1)* 10**state.FRBdemo.lC + pdm = np.sum(g.rates,axis=0)* 10**state.FRBdemo.lC + + return daily,pz,pdm,THRESH + +main() + + diff --git a/papers/lsst/Data/dsa_hosts.csv b/papers/lsst/Data/dsa_hosts.csv index 5abf1c48..e3f31bc9 100644 --- a/papers/lsst/Data/dsa_hosts.csv +++ b/papers/lsst/Data/dsa_hosts.csv @@ -29,3 +29,12 @@ z,mr,phost 0.5530,22.61,0.99 0.6214,21.22,0.97 0.9750,23.17,0.92 +0.1185,19.33,0.99 +0.21,20.36,0.99 +0.262,22.7,0.99 +0.3355,20.49,0.99 +0.287,21.12,0.99 +0.376,21.20,0.98 +0.553,22.90,0.99 +0.968,21.91,0.99 +1.354,21.15,0.95 diff --git a/papers/lsst/make_host_z_mag_plot.py b/papers/lsst/make_host_z_mag_plot.py index 6f5e5e15..6d0851ed 100644 --- a/papers/lsst/make_host_z_mag_plot.py +++ b/papers/lsst/make_host_z_mag_plot.py @@ -107,13 +107,13 @@ def make_host_plot(model,opfile): sig2us[i] = mrvals[sig2u] plt.figure() - plt.plot(zvals,medians,linestyle="-",color="red",label="Mean") + plt.plot(zvals,medians,linestyle="-",color="red",label="Mean $m_r$") plt.plot(zvals,sig1ds,linestyle="--",color=plt.gca().lines[-1].get_color(),label="67% C.I.") plt.plot(zvals,sig1us,linestyle="--",color=plt.gca().lines[-1].get_color()) plt.plot(zvals,sig2ds,linestyle=":",color=plt.gca().lines[-1].get_color(),label="95% C.I.") plt.plot(zvals,sig2us,linestyle=":",color=plt.gca().lines[-1].get_color()) plt.xlabel("z") - plt.ylabel("$m_r$") + plt.ylabel("$m$") z,mr,w = g.read_craft() OK = np.where(w>= 0.5)[0] @@ -127,13 +127,14 @@ def make_host_plot(model,opfile): OK = np.where(w>= 0.5)[0] plt.scatter(z[OK],mr[OK],marker='o',label="DSA",s=20) + zmax=2 plt.ylim(10,30) - plt.xlim(0,1.5) + plt.xlim(0,zmax) Rlim1=24.7 Rlim2=27.5 - plt.plot([0,1.5],[Rlim1,Rlim1],linestyle=":",color="black") - plt.plot([0,1.5],[Rlim2,Rlim2],linestyle=":",color="black") + plt.plot([0,zmax],[Rlim1,Rlim1],linestyle=":",color="black") + plt.plot([0,zmax],[Rlim2,Rlim2],linestyle=":",color="black") plt.text(0.1,Rlim1+0.2,"$m_r^{\\rm lim}=$"+str(Rlim1)) plt.text(0.1,Rlim2+0.2,"$m_r^{\\rm lim}=$"+str(Rlim2)) diff --git a/papers/lsst/sim_pz.py b/papers/lsst/sim_pz.py index 8a148a37..07a1c59f 100644 --- a/papers/lsst/sim_pz.py +++ b/papers/lsst/sim_pz.py @@ -48,7 +48,7 @@ def main(): meerkat_z,meerkat_mr,meerkat_w = read_meerkat() # we should re-do this shortly. - Load=True + Load=False repeaters=False Test=False # do this for very simplified data Scat=False # do not use updated scattering model diff --git a/papers/pathpriors/pU_g_mr/pu_mr_vs_mag_CRAFT_VLT_FORS2_r.csv b/papers/pathpriors/pU_g_mr/pu_mr_vs_mag_CRAFT_VLT_FORS2_r.csv new file mode 100644 index 00000000..de340cd4 --- /dev/null +++ b/papers/pathpriors/pU_g_mr/pu_mr_vs_mag_CRAFT_VLT_FORS2_r.csv @@ -0,0 +1,28 @@ +mag,PU_mr +16.25,0.0 +16.75,0.0 +17.25,0.0 +17.75,0.0 +18.25,0.0 +18.75,0.0 +19.25,0.0 +19.75,0.0 +20.25,0.0 +20.75,0.0 +21.25,0.0 +21.75,0.0 +22.25,0.0 +22.75,0.0 +23.25,0.0 +23.75,0.0 +24.25,0.0 +24.75,0.0 +25.25,0.0 +25.75,0.07053158399229198 +26.25,0.40295047266822037 +26.75,0.7659624379851182 +27.25,0.9680433810752437 +27.75,0.9999032120695396 +28.25,1.0 +28.75,1.0 +29.25,1.0 diff --git a/papers/pathpriors/pU_g_mr/test_pogmr.py b/papers/pathpriors/pU_g_mr/test_pogmr.py new file mode 100644 index 00000000..2ac80e14 --- /dev/null +++ b/papers/pathpriors/pU_g_mr/test_pogmr.py @@ -0,0 +1,65 @@ +""" + +This script fits data to the p(U|mr) data, i.e. the probability that +a galaxy is unseen given it has an intrinsic magnitude of m_r. + +The functional form of the fits is given by opt.pogm + +The data is provided by Michelle Woodland (for CRAFT) +and Bridget Anderson (data to be published) + + +""" + + + +from zdm import optical as opt +from matplotlib import pyplot as plt +import numpy as np +import pandas as pd +from scipy.optimize import curve_fit + +# relevant for CRAFT, from Bridget +df = pd.read_csv("pu_mr_vs_mag_CRAFT_VLT_FORS2_r.csv") +result = curve_fit(opt.pogm,df['mag'],df['PU_mr'],p0=[26.5,0.3]) +#CRAFT_result = result[0] +CRAFT_pogm = opt.pogm(df['mag'],result[0][0],result[0][1]) + +# Legacy surveys - from Bridget Anderson +Lmags = [11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 20.0, + 21.0, 22.0, 23.0, 24.0, 25.0, 26.0, 27.0] +LpU_mr = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.027122579985493736, + 0.02436363700867187, 0.014231286256411943, 0.047649506708623335, + 0.15510269056554593, 0.4759090774425562, 0.8798642289140987, 1.0, + 0.980904733057884] +result = curve_fit(opt.pogm,Lmags,LpU_mr,p0=[26.5,0.3]) +Legacy_result = result[0] +Legacy_fit = opt.pogm(Lmags,result[0][0],result[0][1]) + +#Pan-STARRS: from Bridget Anderson +PSmags = [11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 20.0, + 21.0, 22.0, 23.0, 24.0, 25.0, 26.0, 27.0] +PSpU_mr = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.05338978533359865, + 0.0753794861936344, 0.17783737932531407, 0.6123903316329317, + 0.9170697731444298, 0.9736154053312236, 1.0, 0.9704756326566667, + 0.9993072645593615] +result = curve_fit(opt.pogm,PSmags,PSpU_mr,p0=[26.5,0.3]) +PS_result = result[0] +PS_fit = opt.pogm(PSmags,result[0][0],result[0][1]) + +plt.figure() + +plt.plot(df['mag'],df['PU_mr'],label="CRAFT data") +plt.plot(df['mag'],CRAFT_pogm,label=" fit",linestyle="--",color = plt.gca().lines[-1].get_color()) +plt.plot(Lmags,LpU_mr,label="Legacy data") +plt.plot(Lmags,Legacy_fit,label=" fit",linestyle="--",color = plt.gca().lines[-1].get_color()) +plt.plot(PSmags,PSpU_mr,label="Pan-STARRS data") +plt.plot(PSmags,PS_fit,label=" fit",linestyle="--",color = plt.gca().lines[-1].get_color()) +plt.legend() +plt.xlim(15,30) +plt.xlabel("$m_r$") +plt.ylabel("$p(U|m_r)$") +plt.tight_layout() +plt.savefig("pogm.png") +plt.close() + diff --git a/papers/pathpriors/plot_craft_optical_data.py b/papers/pathpriors/plot_craft_optical_data.py new file mode 100644 index 00000000..1b4bef90 --- /dev/null +++ b/papers/pathpriors/plot_craft_optical_data.py @@ -0,0 +1,143 @@ +""" +This file generates plots of the CRAFT host galaxy candidates +""" + + +#standard Python imports +import os +import numpy as np +import pandas as pd +from matplotlib import pyplot as plt +from importlib import resources +from scipy.integrate import quad + +# imports from the "FRB" series +from zdm import optical as opt +from frb.frb import FRB +from astropath import chance + +import matplotlib + +defaultsize=14 +ds=4 +font = {'family' : 'Helvetica', + 'weight' : 'normal', + 'size' : defaultsize} +matplotlib.rc('font', **font) + + + +def main(): + """ + Main function + Contains outer loop to iterate over parameters + + """ + + # gets this + frblist = opt.frblist + + maglist=[] + anglist=[] + + + for i,frb in enumerate(frblist): + my_frb = FRB.by_name(frb) + # reads in galaxy info + ppath = os.path.join(resources.files('frb'), 'data', 'Galaxies', 'PATH') + pfile = os.path.join(ppath, f'{my_frb.frb_name}_PATH.csv') + ptbl = pd.read_csv(pfile) + candidates = ptbl[['ang_size', 'mag', 'ra', 'dec', 'separation']] + + maglist = maglist + list(candidates['mag'].values) + anglist = anglist + list(candidates['ang_size'].values) + + + ngs = len(maglist) + weights = np.full([ngs],1.) + + # gets most likely ics hosts + z,mr,w = read_craft() + weights = np.append(weights,-w) + tempmaglist = np.append(maglist,mr) + + + + # plots histograms + NM=21 + mags = np.linspace(10,30,NM) + mids = (mags[1:] + mags[:-1])/2. + #pmags = np.zeros([NM-1]) + #for i,mag in enumerate(mids): + # pmags[i] = chance.driver_sigma(mag) + pmags = int_driver(mags) + + plt.figure() + plt.hist(maglist,label="optical images",bins=mags) + plt.hist(tempmaglist,label="'host' subtracted",weights=weights,bins=mags) + plt.xlabel("Apparent magnitude, $m$") + plt.ylabel("Number of galaxies") + + # arbitrary normalisation + pmags = 2.*pmags*80/pmags[12] + + plt.plot(mids,pmags,label="Driver et al. 2016",linewidth=3,linestyle="--") + plt.legend() + plt.xlim(14,28) + plt.xticks(np.linspace(14,28,8)) + plt.ylim(0,100) + plt.tight_layout() + plt.savefig("Figures/mag_hist.png") + plt.close() + + # gets the ratio + h,b = np.histogram(maglist,bins=mags) + ratio = h/pmags + + plt.figure() + plt.plot(mids,ratio) + plt.xlabel("Apparent magnitude, $m$") + plt.ylabel("ratio: CRAFT/Driver et al") + plt.yscale("log") + plt.tight_layout() + plt.savefig("Figures/ratio.png") + plt.close() + + plt.figure() + plt.scatter(maglist,anglist) + plt.xlabel("Apparent magnitude, $m$") + plt.ylabel("Angular size, $\\theta$ [']") + plt.tight_layout() + plt.savefig("Figures/ang_mag.png") + plt.close() + + +def int_driver(bins): + """ + Integrates the driver et al formula over the magnitude bins + + Args: + bins: bins in r-band magnitude + """ + + nbins = len(bins) + integral = np.zeros([nbins-1]) + for i in np.arange(nbins-1): + result = quad(chance.driver_sigma,bins[i],bins[i+1]) + integral[i] = result[0] + return integral + +def read_craft(): + """ + CRAFT ICS data + """ + + DF = pd.read_csv("../lsst/Data/craft_ics_hosts.csv") + + z = np.array(DF["z"]) + mr = np.array(DF["mr"]) + nfrb = len(mr) + w = np.full([nfrb],1.) # artificial, but all are highy confidence + return z,mr,w + +main() diff --git a/zdm/optical.py b/zdm/optical.py index 1ca52f8e..e5c08af5 100644 --- a/zdm/optical.py +++ b/zdm/optical.py @@ -498,8 +498,13 @@ def __init__(self,OpticalState=None,verbose=False): if self.opstate.AppModelID == 0: if verbose: print("Initialising simple luminosity function") - # must take arguments of (absoluteMag,z) + # must take arguments of (absoluteMag,k,z) self.CalcApparentMags = SimpleApparentMags + elif self.opstate.AppModelID == 1: + if verbose: + print("Initialising k-corrected luminosity function") + # must take arguments of (absoluteMag,k,z) + self.CalcApparentMags = SimplekApparentMags else: raise ValueError("Model ",self.opstate.AppModelID," not implemented") @@ -524,16 +529,37 @@ def __init__(self,OpticalState=None,verbose=False): # could perhaps use init args for this? if self.opstate.AbsPriorMeth==0: # uniform prior in log space of absolute magnitude - Absprior = np.full([self.ModelNBins],1./self.NAbsBins) + AbsPrior = np.full([self.ModelNBins],1./self.NAbsBins) else: # other methods to be added as required raise ValueError("Luminosity prior method ",self.opstate.AbsPriorMeth," not implemented") - self.init_args(Absprior) + # enforces normalisation of the prior to unity + self.AbsPrior = AbsPrior/np.sum(AbsPrior) + + # k-correction + self.k = self.opstate.k + + # this maps the weights from the parameter file to the absoluate magnitudes use + # internally within the program. We now initialise this during an "init" + self.init_abs_mag_weights() # the below is done for the wrapper function #self.ZMAP = False # records that we need to initialise this + def get_args(self): + """ + function to return args as a vector in the form of init_args + """ + + if self.opstate.AppModelID == 0: + args = self.AbsPrior + elif self.opstate.AppModelID == 1: + args = np.zeros([self.ModelNBins+1]) + args[1:] = self.AbsPrior + args[0] = self.k + return args + def init_abs_bins(self): """ Initialises internal array of absolute magnitudes @@ -559,19 +585,27 @@ def init_abs_bins(self): self.MagBins = MagBins self.dMag = dMag self.AbsMags = AbsMags + - def init_args(self,AbsPrior): + def init_args(self,Args): """ Initialises prior on absolute magnitude of galaxies according to the method. Args: - - AbsPrior (list of floats): The prior on absolute magnitudes + - Args (list of floats): The prior on absolute magnitudes to set for this model + IF AppModelID == 1, then interpret the first as the k-correction """ # Eventually, incorporate the AbsPrior vector into SimpleParams #self.opstate = OpticalState.SimpleParams + if self.opstate.AppModelID == 0: + AbsPrior=Args + elif self.opstate.AppModelID == 1: + AbsPrior=Args[1:] + self.k = Args[0] + # enforces normalisation of the prior to unity self.AbsPrior = AbsPrior/np.sum(AbsPrior) @@ -629,7 +663,10 @@ def get_pmr_gz(self,mrbins,z): """ # mapping of apparent to absolute magnitude - mrvals = self.CalcApparentMags(self.AbsMags,z) # works with scalar z + if self.opstate.AppModelID == 0: + mrvals = self.CalcApparentMags(self.AbsMags,z) # works with scalar z + elif self.opstate.AppModelID == 1: + mrvals = self.CalcApparentMags(self.AbsMags,self.k,z) # works with scalar z # creates weighted histogram of apparent magnitudes, @@ -753,6 +790,9 @@ def __init__(self,model,zvals): # higher level state defining optical parameters self.OpticalState = model.OpticalState + self.pU_mean = self.OpticalState.id.pU_mean + self.pU_width = self.OpticalState.id.pU_width + # specific substate of the model self.opstate = model.opstate @@ -857,7 +897,13 @@ def init_path_raw_prior_Oi(self,DM,grid): # stores knowledge of the DM used to calculate the priors self.prior_DM = DM - self.priors = priors + self.raw_priors = priors + + pUgm = pU_g_mr = pogm(self.AppMags,self.pU_mean,self.pU_width) + + self.priors = self.raw_priors * (1.-pUgm) + self.PU = self.raw_priors * pUgm + # sets the PATH user function to point to its own pathpriors.USR_raw_prior_Oi = self.path_raw_prior_Oi @@ -892,9 +938,9 @@ def get_posterior(self, grid, DM): return papps,pz - - def estimate_unseen_prior(self,mag_limit): + + def estimate_unseen_prior(self): """ Calculates PU, the prior that an FRB host galaxy of a particular DM is unseen in the optical image @@ -916,9 +962,18 @@ def estimate_unseen_prior(self,mag_limit): """ - invisible = np.where(self.AppMags > mag_limit)[0] + # smooth cutoff + #pU_g_mr = pogm(self.AppMags,mean,width) + + # simple hard cutoff - now redundant + #invisible = np.where(self.AppMags > mag_limit)[0] - PU = np.sum(self.priors[invisible]) + #PU = np.sum(pU_g_mr * self.priors) + + #PU = np.sum(self.priors[invisible]) + + # we now pre-calculate this at the init raw path prior stage + PU = np.sum(self.PU) return PU @@ -1024,6 +1079,57 @@ def get_pz_prior(grid, DM): pz = pz/np.sum(pz,axis=0) return pz + +def SimplekApparentMags(Abs,k,zs): + """ + Function to convert galaxy absolue to apparent magnitudes. + Same as simple apparent mags, but allows for a k-correction. + + Nominally, magnitudes are r-band magnitudes, but this function + is so simple it doesn't matter. + + Just applies a distance correction - no k-correction. + + Args: + Abs (float or array of floats): intrinsic galaxy luminosities + k (float): k-correction + zs (float or array of floats): redshifts of galaxies + + Returns: + ApparentMags: NAbs x NZ array of magnitudes, where these + are the dimensions of the inputs + """ + + # calculates luminosity distances (Mpc) + lds = cos.dl(zs) + + # finds distance relative to absolute magnitude distance + dabs = 1e-5 # in units of Mpc + + # k-corrections + kcorrs = (1+zs)**k + + # relative magnitude + dMag = 2.5*np.log10((lds/dabs)**(2)) + 2.5*np.log10(kcorrs) + + + + + if np.isscalar(zs) or np.isscalar(Abs): + # just return the product, be it scalar x scalar, + # scalar x array, or array x scalar + # this also ensures that the dimensions are as expected + + ApparentMags = Abs + dMag + else: + # Convert to multiplication so we can use + # numpy.outer + temp1 = 10**Abs + temp2 = 10**dMag + ApparentMags = np.outer(temp1,temp2) + ApparentMags = np.log10(ApparentMags) + return ApparentMags + def SimpleApparentMags(Abs,zs): """ Function to convert galaxy absolue to apparent magnitudes. @@ -1172,90 +1278,6 @@ def matchFRB(TNSname,survey): -def run_path(name,PU=0.1,usemodel = False, sort = False): - """ - evaluates PATH on an FRB - - absolute [bool]: if True, treats rel_error as an absolute value - in arcseconds - """ - from frb.frb import FRB - from astropath.priors import load_std_priors - from astropath.path import PATH - - ######### Loads FRB, and modifes properties ######### - my_frb = FRB.by_name(name) - - # do we even still need this? I guess not, but will keep it here just in case - my_frb.set_ee(my_frb.sig_a,my_frb.sig_b,my_frb.eellipse['theta'], - my_frb.eellipse['cl'],True) - - # reads in galaxy info - ppath = os.path.join(resources.files('frb'), 'data', 'Galaxies', 'PATH') - pfile = os.path.join(ppath, f'{my_frb.frb_name}_PATH.csv') - ptbl = pandas.read_csv(pfile) - - # Load prior - priors = load_std_priors() - prior = priors['adopted'] # Default - - theta_new = dict(method='exp', - max=priors['adopted']['theta']['max'], - scale=0.5) - prior['theta'] = theta_new - - # change this to something depending on the FRB DM - prior['U']=PU - - candidates = ptbl[['ang_size', 'mag', 'ra', 'dec', 'separation']] - - this_path = PATH() - this_path.init_candidates(candidates.ra.values, - candidates.dec.values, - candidates.ang_size.values, - mag=candidates.mag.values) - this_path.frb = my_frb - - frb_eellipse = dict(a=my_frb.sig_a, - b=my_frb.sig_b, - theta=my_frb.eellipse['theta']) - - this_path.init_localization('eellipse', - center_coord=this_path.frb.coord, - eellipse=frb_eellipse) - - # this results in a prior which is uniform in log space - # when summed over all galaxies with the same magnitude - if usemodel: - this_path.init_cand_prior('user', P_U=prior['U']) - else: - this_path.init_cand_prior('inverse', P_U=prior['U']) - - # this is for the offset - this_path.init_theta_prior(prior['theta']['method'], - prior['theta']['max'], - prior['theta']['scale']) - - P_O=this_path.calc_priors() - - # Calculate p(O_i|x) - debug = True - P_Ox,P_U = this_path.calc_posteriors('fixed', - box_hwidth=10., - max_radius=10., - debug=debug) - mags = candidates['mag'] - - if sort: - indices = np.argsort(P_Ox) - P_O = P_O[indices] - P_Ox = P_Ox[indices] - mags = mags[indices] - - return P_O,P_Ox,P_U,mags - - - def plot_frb(name,ralist,declist,plist,opfile): """ @@ -1320,4 +1342,25 @@ def plot_frb(name,ralist,declist,plist,opfile): - +def pogm(mag,mean,width): + """ + Function to describe probability of identifying a galaxy in + an optical image as a function of its magnitude + + Args: + mag (float or array of floats): magnitude(s) at which + to evaluate the function + mean: magnitude at which the probability is 50% + width: characteristic width of transition from 0-50 and + 50-100 % + """ + + # converts to a number relative to the mean. Will be weird for mags < 0. + diff = (mag-mean)/width + + # converts the diff to a power of 10 + pmr = 1.-1./(1+np.exp(diff)) + + return pmr + + diff --git a/zdm/optical_numerics.py b/zdm/optical_numerics.py index ac8d1e61..8839f0d2 100644 --- a/zdm/optical_numerics.py +++ b/zdm/optical_numerics.py @@ -4,9 +4,13 @@ for the grid. """ +import os +from importlib import resources import numpy as np -from zdm import optical as op from matplotlib import pyplot as plt +import pandas + +from zdm import optical as op def function(x,args): """ @@ -21,6 +25,7 @@ def function(x,args): gs: list of grids corresponding to those surveys model: optical model class which takes arguments x to be minimised. i.e. the function call model.AbsPrior = x must fully specify the model. + istat [int]: which stat to use? 0 = ks stat. 1 = mak likelihood """ @@ -30,6 +35,7 @@ def function(x,args): gs=args[2] model=args[3] POxcut=args[4] # either None, or a cut such as 0.9 + istat=args[5] # initialises model to the priors # generates one per grid, due to possible different zvals @@ -37,14 +43,20 @@ def function(x,args): model.init_args(x) wrappers = make_wrappers(model,gs) + print("calculating arguments ",x) + NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUprior,PUobs,sumPUprior,sumPUobs = calc_path_priors(frblist,ss,gs,wrappers,verbose=False) # we re-normalise the sum of PUs by NFRB # prevents infinite plots being created - stat = calculate_goodness_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors, + if istat==0: + stat = calculate_ks_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors, sumPUobs,sumPUprior,plotfile=None,POxcut=POxcut) - + elif istat==1: + stat = calculate_likelihood_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors, + sumPUobs,sumPUprior,plotfile=None,POxcut=POxcut) + return stat def make_wrappers(model,grids): @@ -103,9 +115,16 @@ def calc_path_priors(frblist,ss,gs,wrappers,verbose=True,usemodel=True): NFRB = len(frblist) - allObsMags = None - allPOx = None - allpriors = None + # old version creating 1D lists + #allObsMags = None + #allPOx = None + #allMagPriors = None + + # new version recording one list per FRB. For max likelihood functionality + allObsMags = [] + allPOx = [] + allMagPriors = [] + sumPU = 0. sumPUx = 0. allPU = [] @@ -161,30 +180,49 @@ def calc_path_priors(frblist,ss,gs,wrappers,verbose=True,usemodel=True): bad = np.where(AppMags > mag_limit)[0] MagPriors[bad] = 0. - P_O,P_Ox,P_Ux,ObsMags = op.run_path(frb,usemodel=usemodel,PU = PU) + P_O,P_Ox,P_Ux,ObsMags,ptbl = run_path(frb,usemodel=usemodel,PU = PU) + + if i==0: + allgals = ptbl + else: + allgals = pandas.concat([allgals,ptbl], ignore_index=True) ObsMags = np.array(ObsMags) - if allObsMags is None: - allObsMags = ObsMags - allPOx = P_Ox - allMagPriors = MagPriors - else: - allObsMags = np.append(allObsMags,ObsMags) - allPOx = np.append(allPOx,P_Ox) - allMagPriors += MagPriors + # old version creating a 1D list + #if allObsMags is None: + # allObsMags = ObsMags + # allPOx = P_Ox + # allMagPriors = MagPriors + #else: + # allObsMags = np.append(allObsMags,ObsMags) + # allPOx = np.append(allPOx,P_Ox) + # allMagPriors += MagPriors + + # new version creating a list of lists + allObsMags.append(ObsMags) + allPOx.append(P_Ox) + allMagPriors.append(MagPriors) sumPU += PU sumPUx += P_Ux allPU.append(PU) allPUx.append(P_Ux) + + + subset = allgals[['frb','mag','VLT_FORS2_R']].copy() + + # saves all galaxies + if not os.path.exists("allgalaxies.csv"): + subset.to_csv("allgalaxies.csv",index=False) + exit() return nfitted,AppMags,allMagPriors,allObsMags,allPOx,allPU,allPUx,sumPU,sumPUx -def calculate_goodness_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUobs, +def calculate_likelihood_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUobs, PUprior,plotfile=None,POxcut=None): """ - Calculates a ks-like statistics to be proxy for goodness-of-fit + Calculates a likelihood for each of the FRBs, and returns the log-likelihood. We must set each AppMagPriors to 1.-PUprior at the limiting magnitude for each observation, and sum the ObsPosteriors to be equal to 1.-PUobs at that magnitude. Then these are what gets summed. @@ -206,19 +244,67 @@ def calculate_goodness_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors k-like statistic of biggest obs/prior difference """ + # calculates log-likelihood of observation + stat=0 + for i in np.arange(NFRB): + # sums the likelihoods over each galaxy: p(xi|oi)*p(oi)/Pfield + sumpost = np.sum(ObsPosteriors[i]) + ll = np.log10(sumpost) + stat += ll + + return stat + +def flatten(xss): + """ + Turns a list of lists into a single list + """ + return [x for xs in xss for x in xs] + +def calculate_ks_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUobs, + PUprior,plotfile=None,POxcut=None): + """ + Calculates a ks-like statistic to be proxy for goodness-of-fit + We must set each AppMagPriors to 1.-PUprior at the limiting magnitude for each observation, + and sum the ObsPosteriors to be equal to 1.-PUobs at that magnitude. + Then these are what gets summed. + + This can be readily done by combining all ObsMags and ObsPosteriors into a single long list, + since this should already be correctly normalised. Priors require their own weight. + + Inputs: + AppMags: array listing apparent magnitudes + AppMagPrior: array giving prior on AppMags + ObsMags: list of observed magnitudes + ObsPosteriors: list of posterior values corresponding to ObsMags + PUobs: posterior on unseen probability + PUprior: prior on PU + plotfile: set to name of output file for comparison plot + POxcut: if not None, cut data to fixed POx. Used to simulate current techniques + + Returns: + k-like statistic of biggest obs/prior difference + """ + + # creates flattened lists + fAppMagPrios = np.array(flatten(AppMagPriors)) + + fObsPosteriors = np.array(flatten(ObsPosteriors)) + + fObsMags = np.array(flatten(ObsMags)) + # we calculate a probability using a cumulative distribution - prior_dist = np.cumsum(AppMagPriors) + prior_dist = np.cumsum(fAppMagPriors) if POxcut is not None: # cuts data to "good" FRBs only - OK = np.where(ObsPosteriors > POxcut)[0] + OK = np.where(fObsPosteriors > POxcut)[0] Ndata = len(OK) - ObsMags = ObsMags[OK] - ObsPosteriors = np.full([Ndata],1.) # effectively sets these to unity + fObsMags = fObsMags[OK] + fObsPosteriors = np.full([Ndata],1.) # effectively sets these to unity # makes a cdf in units of AppMags, with observations ObsMags weighted by ObsPosteriors - obs_dist = make_cdf(AppMags,ObsMags,ObsPosteriors,norm=False) + obs_dist = make_cdf(AppMags,fObsMags,fObsPosteriors,norm=False) if POxcut is not None: # current techniques just assume we have the full distribution @@ -248,3 +334,91 @@ def calculate_goodness_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors return stat + + + +def run_path(name,PU=0.1,usemodel = False, sort = False): + """ + evaluates PATH on an FRB + + absolute [bool]: if True, treats rel_error as an absolute value + in arcseconds + """ + from frb.frb import FRB + from astropath.priors import load_std_priors + from astropath.path import PATH + + ######### Loads FRB, and modifes properties ######### + my_frb = FRB.by_name(name) + + # do we even still need this? I guess not, but will keep it here just in case + my_frb.set_ee(my_frb.sig_a,my_frb.sig_b,my_frb.eellipse['theta'], + my_frb.eellipse['cl'],True) + + # reads in galaxy info + ppath = os.path.join(resources.files('frb'), 'data', 'Galaxies', 'PATH') + pfile = os.path.join(ppath, f'{my_frb.frb_name}_PATH.csv') + ptbl = pandas.read_csv(pfile) + + ngal = len(ptbl) + ptbl["frb"] = np.full([ngal],name) + + # Load prior + priors = load_std_priors() + prior = priors['adopted'] # Default + + theta_new = dict(method='exp', + max=priors['adopted']['theta']['max'], + scale=0.5) + prior['theta'] = theta_new + + # change this to something depending on the FRB DM + prior['U']=PU + + candidates = ptbl[['ang_size', 'mag', 'ra', 'dec', 'separation']] + + this_path = PATH() + this_path.init_candidates(candidates.ra.values, + candidates.dec.values, + candidates.ang_size.values, + mag=candidates.mag.values) + this_path.frb = my_frb + + frb_eellipse = dict(a=my_frb.sig_a, + b=my_frb.sig_b, + theta=my_frb.eellipse['theta']) + + this_path.init_localization('eellipse', + center_coord=this_path.frb.coord, + eellipse=frb_eellipse) + + # this results in a prior which is uniform in log space + # when summed over all galaxies with the same magnitude + if usemodel: + this_path.init_cand_prior('user', P_U=prior['U']) + else: + this_path.init_cand_prior('inverse', P_U=prior['U']) + + # this is for the offset + this_path.init_theta_prior(prior['theta']['method'], + prior['theta']['max'], + prior['theta']['scale']) + + P_O=this_path.calc_priors() + + # Calculate p(O_i|x) + debug = True + P_Ox,P_U = this_path.calc_posteriors('fixed', + box_hwidth=10., + max_radius=10., + debug=debug) + mags = candidates['mag'] + + if sort: + indices = np.argsort(P_Ox) + P_O = P_O[indices] + P_Ox = P_Ox[indices] + mags = mags[indices] + + return P_O,P_Ox,P_U,mags,ptbl + diff --git a/zdm/optical_params.py b/zdm/optical_params.py index de98e454..e5ca6fd8 100644 --- a/zdm/optical_params.py +++ b/zdm/optical_params.py @@ -55,7 +55,7 @@ class SimpleParams(data_class.myDataClass): }) AppModelID: int = field( default=0, - metadata={'help': "Model for converting absolute to apparent magnitudes. 0: no k-correction. Others to be implemented.", + metadata={'help': "Model for converting absolute to apparent magnitudes. 0: no k-correction. 1: with k-correctionOthers to be implemented.", 'unit': '', 'Notation': '', }) @@ -65,6 +65,12 @@ class SimpleParams(data_class.myDataClass): 'unit': '', 'Notation': '', }) + k: float = field( + default=0., + metadata={'help': "k-correction", + 'unit': '', + 'Notation': 'k', + }) # Nick Loudas's SFR model @@ -118,6 +124,24 @@ class LoudasParams(data_class.myDataClass): }) +@dataclass +class Identification(data_class.myDataClass): + """ + # parameters for identifying galaxies in an image + """ + pU_mean: float = field( + default=26.385, + metadata={'help': "Magnitude at which pU|mr is 0.5", + 'unit': '', + 'Notation': '', + }) + pU_width: float = field( + default=0.279, + metadata={'help': "Width of pU|mr distribution in ln space", + 'unit': '', + 'Notation': '', + }) + @dataclass class Apparent(data_class.myDataClass): """ @@ -156,6 +180,7 @@ def set_dataclasses(self): self.simple = SimpleParams() self.loudas = LoudasParams() self.app = Apparent() + self.id = Identification() def update_param(self, param:str, value): diff --git a/zdm/scripts/Path/optimise_host_priors.py b/zdm/scripts/Path/optimise_host_priors.py index 91cf55de..6b204eef 100644 --- a/zdm/scripts/Path/optimise_host_priors.py +++ b/zdm/scripts/Path/optimise_host_priors.py @@ -31,6 +31,7 @@ from zdm import parameters from zdm import loading from zdm import optical_numerics as on +from zdm import states # other FRB library imports import astropath.priors as pathpriors @@ -52,16 +53,21 @@ def main(): # be re-fed into FRB surveys. However, it will be difficult to do this # with very limited redshift estimates. That might require posterior # estimates of redshift given the observed galaxies. Maybe. - state = parameters.State() + state = states.load_state("HoffmannHalo25",scat=None,rep=None) + #state = parameters.State() cos.set_cosmology(state) cos.init_dist_measures() names=['CRAFT_ICS_892','CRAFT_ICS_1300','CRAFT_ICS_1632'] - ss,gs = loading.surveys_and_grids(survey_names=names) + ss,gs = loading.surveys_and_grids(survey_names=names,init_state=state) modelname = "loudas" + modelname = "simple" + opdir = modelname+"_0.9_output/" POxcut = None # set to e.g. 0.9 to reject FRBs with lower posteriors when doing model comparisons + + if not os.path.exists(opdir): os.mkdir(opdir) @@ -69,8 +75,12 @@ def main(): # Initialisation of model # simple host model if modelname=="simple": - model = opt.simple_host_model() - x0 = model.AbsPrior + opstate = op.OpticalState() + opstate.simple.AppModelID = 1 # sets to include k-correction + opstate.simple.k = 1. + model = opt.simple_host_model(opstate) + x0 = model.get_args() + elif modelname=="loudas": #### case of Loudas model model = opt.loudas_model() @@ -79,8 +89,13 @@ def main(): print("Unrecognised host model ", modelname) + # setting istat=0 means using a ks statistic to fit p(m_r) + istat=0 + # setting istat=1 means using a maximum likelihood estimator + istat=1 + # initialise aguments to minimisation function - args=[frblist,ss,gs,model,POxcut] + args=[frblist,ss,gs,model,POxcut,istat] Nparams = len(x0) bounds = [(0,1)]*Nparams @@ -141,7 +156,7 @@ def main(): plt.xlabel("Absolute magnitude, $M_r$") plt.ylabel("$p(M_r)$") plt.plot(model.AbsMags,model.AbsMagWeights/np.max(model.AbsMagWeights),label="interpolation") - plt.plot(model.ModelBins,x/np.max(x),marker="o",linestyle="",label="Model Parameters") + plt.plot(model.ModelBins,x[1:]/np.max(x[1:]),marker="o",linestyle="",label="Model Parameters") plt.legend() plt.tight_layout() plt.savefig(opdir+"best_fit_absolute_magnitudes.pdf") @@ -171,7 +186,9 @@ def main(): plt.tight_layout() plt.savefig(outfile) plt.close() - + + + def make_cdf_for_plotting(xvals,weights=None): """ Creates a cumulative distribution function diff --git a/zdm/scripts/Path/plot_host_models.py b/zdm/scripts/Path/plot_host_models.py index bce29496..ba52c462 100644 --- a/zdm/scripts/Path/plot_host_models.py +++ b/zdm/scripts/Path/plot_host_models.py @@ -17,6 +17,7 @@ # imports from the "FRB" series from zdm import optical as opt from zdm import optical_params as op +from zdm import optical_numerics as on from zdm import loading from zdm import cosmology as cos from zdm import parameters @@ -111,8 +112,9 @@ def calc_path_priors(): plt.close() # set up basic histogram of p(mr) distribution - mrbins = np.linspace(0,30,301) + mrbins = np.linspace(0,40,401) mrvals=(mrbins[:-1]+mrbins[1:])/2. + dmr = mrbins[1]-mrbins[0] model3 = opt.marnoch_model() @@ -120,29 +122,32 @@ def calc_path_priors(): styles=["-","--",":","-."] plt.figure() - flist=[0,1.] + flist=[0,1] - for z in [0.1,0.5,2]: + for i,z in enumerate([0.1,0.5,2.0]): # simple model pmr = model1.get_pmr_gz(mrbins,z) pmr /= np.sum(pmr) - plt.plot(mrvals,pmr,label="Simple: z = "+str(z),linestyle=styles[0]) + + plt.plot(mrvals,pmr/dmr,label="z = "+str(z)+"; Naive",linestyle=styles[0]) # Loudas model dependencies for i,fsfr in enumerate(flist): model2.init_args(fsfr) pmr = model2.get_pmr_gz(mrbins,z) pmr /= np.sum(pmr) - plt.plot(mrvals,pmr,label = "z = "+str(z)+", $f_{\\rm sfr}$ = "+str(fsfr), + plt.plot(mrvals,pmr/dmr,label = " $f_{\\rm sfr}$ = "+str(fsfr), linestyle=styles[i+1],color=plt.gca().lines[-1].get_color()) pmr = model3.get_pmr_gz(mrbins,z) - plt.plot(mrvals,pmr,label = "Marnoch: z = "+str(z),linestyle=styles[3], + plt.plot(mrvals,pmr/dmr,label = " Marnoch",linestyle=styles[3], color=plt.gca().lines[-1].get_color()) - plt.xlabel("Optical magnitude $m_r$") - plt.ylabel("p(m_r|z)") + plt.xlabel("Apparent magnitude $m_r$") + plt.ylabel("$p(m_r|z)$") + plt.xlim(10,40) + plt.ylim(0,0.35) plt.tight_layout() plt.legend() plt.savefig(opdir+"all_model_apparent_mags.png") @@ -164,6 +169,12 @@ def calc_path_priors(): wrapper2 = opt.model_wrapper(model2,g.zvals) # loudas with fsfr=0 wrapper3 = opt.model_wrapper(model3,g.zvals) # loudas with fsfr=0 + # simply illustrates how one might change the probabilities of + # observaing a galaxy of a given magnitude + wrapper1.pU_mean = 26.385 + wrapper1.pU_width = 0.279 + + # do this once per "model" objects #pathpriors.USR_raw_prior_Oi = wrapper1.path_raw_prior_Oi @@ -236,7 +247,7 @@ def calc_path_priors(): DMEG = s.DMEGs[imatch] # original calculation - P_O1,P_Ox1,P_Ux1,mags1 = opt.run_path(frb,usemodel=False,PU=0.1) + P_O1,P_Ox1,P_Ux1,mags1,ptbl = on.run_path(frb,usemodel=False,PU=0.1) # record this info if maglist[0] is None: @@ -249,9 +260,9 @@ def calc_path_priors(): # simple model wrapper1.init_path_raw_prior_Oi(DMEG,g) - PU2 = wrapper1.estimate_unseen_prior(mag_limit=26) # might not be correct + PU2 = wrapper1.estimate_unseen_prior() # might not be correct pathpriors.USR_raw_prior_Oi = wrapper1.path_raw_prior_Oi - P_O2,P_Ox2,P_Ux2,mags2 = opt.run_path(frb,usemodel=True,PU = PU2) + P_O2,P_Ox2,P_Ux2,mags2,ptbl = on.run_path(frb,usemodel=True,PU = PU2) for imag,mag in enumerate(mags2): @@ -275,9 +286,9 @@ def calc_path_priors(): model2.init_args(fSFR) wrapper2.init_zmapping(g.zvals) wrapper2.init_path_raw_prior_Oi(DMEG,g) - PU3 = wrapper2.estimate_unseen_prior(mag_limit=26) # might not be correct + PU3 = wrapper2.estimate_unseen_prior() # might not be correct pathpriors.USR_raw_prior_Oi = wrapper2.path_raw_prior_Oi - P_O3,P_Ox3,P_Ux3,mags3 = opt.run_path(frb,usemodel=True,PU = PU3) + P_O3,P_Ox3,P_Ux3,mags3,ptbl = on.run_path(frb,usemodel=True,PU = PU3) # record this info if maglist[2] is None: @@ -293,9 +304,9 @@ def calc_path_priors(): model2.init_args(fSFR) wrapper2.init_zmapping(g.zvals) wrapper2.init_path_raw_prior_Oi(DMEG,g) - PU4 = wrapper2.estimate_unseen_prior(mag_limit=26) # might not be correct limit + PU4 = wrapper2.estimate_unseen_prior() # might not be correct limit pathpriors.USR_raw_prior_Oi = wrapper2.path_raw_prior_Oi - P_O4,P_Ox4,P_Ux4,mags4 = opt.run_path(frb,usemodel=True,PU = PU4) + P_O4,P_Ox4,P_Ux4,mags4,ptbl = on.run_path(frb,usemodel=True,PU = PU4) # record this info if maglist[3] is None: @@ -308,9 +319,9 @@ def calc_path_priors(): # Marnoch model wrapper3.init_zmapping(g.zvals) wrapper3.init_path_raw_prior_Oi(DMEG,g) - PU5 = wrapper3.estimate_unseen_prior(mag_limit=26) # might not be correct limit + PU5 = wrapper3.estimate_unseen_prior() # might not be correct limit pathpriors.USR_raw_prior_Oi = wrapper3.path_raw_prior_Oi - P_O5,P_Ox5,P_Ux5,mags5 = opt.run_path(frb,usemodel=True,PU = PU5) + P_O5,P_Ox5,P_Ux5,mags5,ptbl = on.run_path(frb,usemodel=True,PU = PU5) # record this info if maglist[4] is None: diff --git a/zdm/states.py b/zdm/states.py index d8c34345..d80dba3c 100644 --- a/zdm/states.py +++ b/zdm/states.py @@ -242,7 +242,7 @@ def set_fit_params(vparams,case): vparams['FRBdemo']['alpha_method'] = 1 vparams['FRBdemo']['source_evolution'] = 0 vparams['FRBdemo']['sfr_n'] = 2.88 - #vparams['FRBdemo']['lC'] = 3.15 # incorrect, check + vparams['FRBdemo']['lC'] = -8.82 vparams['host']['lmean'] = 2.13 vparams['host']['lsigma'] = 0.46 diff --git a/zdm/survey.py b/zdm/survey.py index 291f3cc7..78b4e401 100644 --- a/zdm/survey.py +++ b/zdm/survey.py @@ -245,8 +245,8 @@ def init_widths(self,state=None): wlist = np.logspace(np.log10(self.WMin)+dlogw/2.,np.log10(self.WMax)-dlogw/2.,self.NWbins) wbins[0] -= 3 # ensures we capture low values! else: - wbins[0] = np.log10(self.WMin) - wbins[1] = np.log10(self.WMax) + wbins[0] = self.WMin + wbins[1] = self.WMax dlogw = np.log10(wbins[1]/wbins[0]) wlist = np.array([(self.WMax*self.WMin)**0.5]) self.wbins = wbins @@ -939,11 +939,12 @@ def process_survey_file(self,filename:str, for field in fields(default_frb):\ # checks to see if this is a field in metadata: if so, takes priority if survey_dict is not None and field.name in survey_dict.keys(): - default_vaue = survey_dict[field.name] + default_value = survey_dict[field.name] elif field.name in self.meta.keys(): default_value = self.meta[field.name] else: default_value = getattr(default_frb, field.name) + # now checks for missing data, fills with the default value if field.name in frb_tbl.columns: From e0f08f040f88144b3df6afc3a598067a2047166a Mon Sep 17 00:00:00 2001 From: Clancy James Date: Tue, 27 Jan 2026 09:22:35 +0800 Subject: [PATCH 14/35] minor debug for new prior estimates --- zdm/optical_numerics.py | 9 +++++---- zdm/scripts/Path/optimise_host_priors.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/zdm/optical_numerics.py b/zdm/optical_numerics.py index 8839f0d2..7a497c88 100644 --- a/zdm/optical_numerics.py +++ b/zdm/optical_numerics.py @@ -167,18 +167,19 @@ def calc_path_priors(frblist,ss,gs,wrappers,verbose=True,usemodel=True): # extracts priors as function of absolute magnitude for this grid and DMEG MagPriors = wrapper.priors - mag_limit=26 # might not be correct. TODO! Should be in FRB object + # defunct now + #mag_limit=26 # might not be correct. TODO! Should be in FRB object # calculates unseen prior if usemodel: - PU = wrapper.estimate_unseen_prior(mag_limit) + PU = wrapper.estimate_unseen_prior() else: PU = 0.1 MagPriors[:] = 1./len(MagPriors) # log-uniform priors when no model used # sets magnitude priors to zero when they are above the magnitude limit - bad = np.where(AppMags > mag_limit)[0] - MagPriors[bad] = 0. + #bad = np.where(AppMags > mag_limit)[0] + #MagPriors[bad] = 0. P_O,P_Ox,P_Ux,ObsMags,ptbl = run_path(frb,usemodel=usemodel,PU = PU) diff --git a/zdm/scripts/Path/optimise_host_priors.py b/zdm/scripts/Path/optimise_host_priors.py index 6b204eef..e7ebac08 100644 --- a/zdm/scripts/Path/optimise_host_priors.py +++ b/zdm/scripts/Path/optimise_host_priors.py @@ -13,7 +13,7 @@ WARNING2: To do this properly also requires inputting the posterior POx for host galaxies into zDM! This simulation does not do that either. -WARNING3: this program takes O~1 hr to run +WARNING3: this program can take a while to run, if optimising the simple model. """ From 36776efd58e14112667eb8d663e6bda39449f629 Mon Sep 17 00:00:00 2001 From: Clancy James Date: Wed, 28 Jan 2026 09:19:54 +0800 Subject: [PATCH 15/35] minor debugs to optical, and adding HTR catalogue properties from scattering paper --- .../Scattering/CRAFT_ICS_HTR_Catalogue1.csv | 37 ++++++++++++++++ zdm/optical.py | 16 +++---- zdm/optical_numerics.py | 43 ++++++++++--------- zdm/scripts/Path/optimise_host_priors.py | 33 +++++++------- 4 files changed, 83 insertions(+), 46 deletions(-) create mode 100644 papers/Scattering/CRAFT_ICS_HTR_Catalogue1.csv diff --git a/papers/Scattering/CRAFT_ICS_HTR_Catalogue1.csv b/papers/Scattering/CRAFT_ICS_HTR_Catalogue1.csv new file mode 100644 index 00000000..610410ca --- /dev/null +++ b/papers/Scattering/CRAFT_ICS_HTR_Catalogue1.csv @@ -0,0 +1,37 @@ +,TNS,Nubar,Nant,SNdet,SNoff,DM,DMErr,DMstruct,DMstructErr,Wsnr,W95,Z,TauObs,TauObsErr,TauAlpha,TauAlphaErr,NUTau,Tau1GHz,Tau1GHzErr,Components,L/I,L/IErr,V/I,V/IErr,P/I,P/IErr,RM,RMErr,RMMW,RMMWErr,Calibrator,PATrend,m,NU_c,NU_dc,NU_dcErr,Alpha_dc,Alpha_dcErr,nudctau,nudctauErr,LzLgMax,LzLgMaxErr +0,20180924B,1320.0,24,21.1,77.0,361.75,0.015,361.74,0.14,0.91,2.0,0.3214,0.59,0.01,-3.68,0.04,1297.5,1.56,0.13,s,0.89,0.02,0.09,0.02,0.9,0.02,17.3,0.8,16.5,5.0,VELA,0.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0 +1,20181112A,1297.5,11,19.3,220.0,589.265,0.001,589.265,0.001,0.1,1.2,0.4755,0.023,0.002,-2.0,0.3,1297.5,0.039,0.006,m,0.9,0.0,0.1,0.0,0.9,0.0,10.5,0.4,16.2,5.9,VELA,2.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0 +2,20190102C,1297.5,23,14.0,167.0,364.55,0.015,364.55,0.02,0.076,1.25,0.29,0.027,0.012,-5.5,1.0,1271.5,0.09,0.06,m,0.96,0.02,0.02,0.02,0.97,0.02,-106.1,0.9,26.6,7.7,VELA,2.0,0.41,1271.5,0.6,0.3,10.0,9999.0,101.0,68.0,9999.0,9999.0 +3,20190608B,1271.5,25,16.1,41.0,340.0,1.0,338.7,0.9,4.95,10.8,0.1178,3.83,0.15,-3.37,1.3,1269.5,8.5,1.2,s,1.0,0.04,0.02,0.02,1.0,0.04,353.0,1.0,-24.4,13.3,VELA,2.0,0.78,1271.5,1.4,0.1,5.8,0.5,33700.0,2700.0,6.6,0.5 +4,20190611B,1271.5,20,9.5,27.0,322.4,0.1,322.7,0.34,0.076,1.592,0.378,0.03,0.015,0.3,2.0,1252.7,0.03,0.02,m,0.74,0.04,0.29,0.04,0.8,0.04,17.0,3.0,29.0,10.8,VELA,2.0,0.96,1271.5,1.5,0.2,-2.0,1.0,282.0,146.0,9999.0,9999.0 +5,20190711A,1271.5,28,23.8,46.0,592.0,2.0,587.74,0.025,8.6,10.99,0.522,0.0076,0.002,-2.5,1.1,1172.9,0.011,0.005,m,0.98,0.03,0.14,0.02,0.99,0.03,4.0,1.0,19.4,6.5,VELA,2.0,0.64,1136.9,0.11,0.01,-10.0,5.0,5.2,1.5,9999.0,9999.0 +6,20190714A,1271.5,28,10.7,52.0,504.7,0.3,504.13,0.4,0.86,2.99,0.2365,0.422,0.008,-2.7,0.6,1286.6,0.83,0.05,s,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,VELA,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0 +7,20191001A,919.5,30,37.1,108.0,507.0,0.3,507.0,0.65,5.3,13.468,0.23,4.52,0.03,-4.85,0.3,826.4,1.78,0.04,s,0.53,0.01,0.05,0.01,0.54,0.01,51.1,0.4,23.5,4.3,VELA,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0 +8,20191228B,1271.5,21,22.9,74.0,297.0,1.0,296.0,2.0,7.8,13.596,0.2432,5.85,0.2,-3.6,0.6,1273.0,14.0,1.0,s,0.93,0.02,0.1,0.02,0.94,0.02,11.9,0.9,18.2,6.1,VELA,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0 +9,20200430A,863.5,26,13.9,67.0,379.9,0.55,379.6,0.55,11.0,22.68,0.161,6.5,0.15,-1.45,0.2,863.5,5.25,0.25,s,0.43,0.02,0.04,0.02,0.43,0.02,195.3,0.7,14.5,7.0,2045-1616,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0 +10,20200906A,864.5,7,16.1,347.0,577.8,0.2,577.81,0.01,0.057,0.128,0.3688,0.0315,0.0007,-4.5,0.4,846.4,0.0148,0.0004,s,0.8,0.005,0.073,0.004,0.804,0.005,75.47,0.08,30.3,19.8,VELA,2.0,0.92,1271.5,1.99,0.01,3.0,1.0,394.0,9.0,11600.0,300.0 +11,20210117A,1271.5,21,17.7,112.0,729.2,0.15,729.1,0.35,1.24,3.584,0.214,0.25,0.2,5.0,8.0,1274.5,0.15,0.1,m,0.92,0.02,0.05,0.01,0.92,0.02,-45.8,0.7,3.3,9.2,VELA,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0 +12,20210320C,863.5,24,15.3,161.0,384.6,0.02,384.59,0.02,0.381,0.884,0.28,0.193,0.007,-4.4,0.1,828.4,0.084,0.004,m,0.86,0.008,0.117,0.006,0.868,0.008,288.8,0.2,-2.8,5.7,1644,1.0,0.83,824.2,0.91,0.03,2.0,1.0,480.0,28.0,1150.0,70.0 +13,20210407E,1271.5,15,19.1,131.0,1784.8,0.2,1784.9,0.35,0.743,1.62,9999.0,0.09,0.02,-1.2,1.6,1219.8,0.08,0.03,m,0.97,0.01,0.09,0.01,0.98,0.01,-9.1,0.6,-59.6,28.6,VELA,2.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0 +14,20210912A,1271.5,22,31.2,479.0,1233.72,0.015,1233.7,0.025,0.095,1.612,9999.0,0.048,0.008,-2.5,0.9,1275.6,0.09,0.03,m,0.625,0.005,0.37,0.005,0.726,0.004,6.0,0.5,8.4,3.8,VELA,2.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0 +15,20211127I,1271.5,24,37.9,340.0,234.83,0.08,234.86,7.02,0.229,0.483,0.046946,0.025,0.02,0.0,5.5,1272.5,0.02,0.02,m,0.244,0.003,0.129,0.003,0.276,0.003,-67.0,1.0,-2.9,6.2,N/A,2.0,0.74,1271.5,2.88,0.09,3.3,0.2,450.0,360.0,9999.0,9999.0 +16,20211203C,920.5,24,14.2,47.0,636.2,0.4,635.16,0.935,12.4,25.449,0.3439,1.66,0.16,-9.7,2.4,891.4,0.55,0.1,s,0.57,0.02,0.07,0.03,0.58,0.02,34.3,1.2,-29.2,9.1,1644,0.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0 +17,20211212A,1631.5,24,10.5,45.0,200.0,1.5,200.0,3.5,2.1,5.628,0.0707,1.8,0.1,-2.8,2.3,1490.8,8.0,6.0,s,0.47,0.02,0.09,0.02,0.48,0.02,21.0,7.0,6.0,5.7,1644,0.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0 +18,20220105A,1631.5,22,9.8,42.0,583.0,2.0,581.5,3.35,0.95,2.25,0.2785,0.43,0.01,-2.0,0.8,1649.8,1.2,0.5,m,0.3,0.03,0.05,0.03,0.3,0.03,-1312.0,8.0,3.9,1.5,1644,0.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0 +19,20220501C,863.5,23,14.8,79.0,449.6,0.3,449.1,0.25,6.1,6.9,0.381,0.35,0.25,4.0,8.0,864.5,0.43,0.3,m,0.68,0.02,0.06,0.02,0.69,0.02,35.5,0.3,9.6,4.5,VELA,2.0,0.44,863.5,9.2,0.5,-1.0,2.0,20200.0,14500.0,9999.0,9999.0 +20,20220610A,1271.5,22,23.9,62.0,1458.1,0.4,1457.6,0.85,1.07,2.0,1.015,0.521,0.001,-3.56,0.03,1149.4,0.855,0.008,s,0.98,0.01,0.065,0.007,0.99,0.01,217.0,2.0,11.9,4.9,VELA,0.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0 +21,20220725A,919.5,25,10.9,49.0,290.1,0.35,290.0,0.25,3.43,8.016,0.1926,2.29,0.05,-1.94,0.06,1149.4,1.95,0.05,a,0.58,0.02,0.13,0.03,0.6,0.02,-26.3,0.7,-190.7,49.8,VELA,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0 +22,20220918A,1271.5,24,26.3,21.0,643.0,5.5,660.0,25.0,11.43,13.851,0.491,7.66,0.1,-2.1,0.03,1133.5,10.0,1.1,s,0.15,0.01,0.13,0.02,0.19,0.02,559.0,23.0,14.6,9.8,VELA,0.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0 +23,20221106A,1631.5,21,19.7,133.0,343.2,0.8,343.0,0.3,5.33,6.895,0.2044,0.182,0.006,-0.7,1.3,1649.6,0.25,0.09,s,0.862,0.008,0.078,0.006,0.865,0.008,444.0,1.0,34.7,11.4,VELA,2.0,0.84,1631.5,2.0,1.0,1.4,0.5,2290.0,1150.0,9999.0,9999.0 +24,20230526A,1271.5,22,22.1,88.0,316.2,0.2,316.1,0.2,2.0,2.7,0.157,1.16,0.01,-3.6,0.3,1272.2,2.75,0.1,a,0.391,0.008,0.04,0.008,0.393,0.008,613.0,2.0,9.7,6.1,VELA,2.0,0.81,1271.5,2.6,0.1,-6.0,4.0,18950.0,750.0,63.0,2.0 +25,20230708A,919.5,23,30.5,270.0,411.54,0.045,411.52,0.065,1.14,23.578,0.105,0.24,0.02,-2.84,0.4,920.5,0.21,0.005,m,0.95,0.01,0.39,0.01,1.03,0.01,-7.5,0.4,43.6,10.5,VELA,2.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0 +26,20230718A,1271.5,22,10.9,104.0,476.67,0.09,476.64,0.15,0.55,0.695,0.035,0.117,0.005,-1.6,0.7,1272.2,0.17,0.02,m,0.92,0.02,0.11,0.01,0.92,0.02,243.1,0.6,186.4,50.4,1644,0.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0 +27,20230731A,1271.5,25,16.6,61.0,701.1,0.3,700.73,0.45,0.65,2.655,9999.0,0.45,0.05,-2.3,0.6,1271.8,0.78,0.04,a,0.42,0.02,0.23,0.02,0.48,0.02,268.0,5.0,213.6,67.3,?????,1.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0 +28,20230902A,832.5,22,11.8,113.0,440.1,0.1,440.166,0.02,0.229,0.678,0.3619,0.123,0.002,-2.55,0.08,812.4,0.072,0.002,m,0.91,0.01,0.05,0.01,0.91,0.01,164.8,0.2,10.1,6.3,VELA,1.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0 +29,20231226A,863.5,22,36.7,96.0,329.9,0.1,328.73,1.37,5.3,9.72,9999.0,0.1,0.07,-1.0,3.0,762.8,0.25,0.2,m,0.86,0.02,0.04,0.01,0.86,0.02,428.4,0.3,13.0,6.8,VELA,2.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0 +30,20240201A,920.5,24,13.9,63.0,374.5,0.2,373.514,0.35,3.05,3.901,0.042729,0.78,0.04,-3.9,0.5,915.5,0.46,0.06,m,0.76,0.02,0.09,0.02,0.76,0.02,1275.0,0.4,5.9,6.5,1644,2.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0 +31,20240208A,863.5,14,12.1,21.0,260.2,0.3,259.83,0.12,1.7,10.0,9999.0,1.35,0.25,-2.7,2.1,864.1,1.0,0.45,s,0.94,0.09,0.08,0.08,0.94,0.09,-73.7,1.4,3.9,6.3,1644,0.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0 +32,20240210A,863.5,23,11.6,59.0,283.73,0.05,283.97,0.03,0.305,1.539,0.023686,0.1,0.03,-3.6,0.3,863.5,0.59,0.04,m,0.73,0.02,0.14,0.02,0.74,0.02,-325.0,1.0,0.8,3.4,VELA,2.0,0.71,863.5,2.7,0.1,1.0,2.0,1700.0,500.0,58.0,17.0 +33,20240304A,832.5,24,12.3,44.0,652.6,0.5,653.4,4.35,8.57,19.0,9999.0,2.51,0.12,3.5,1.3,877.0,4.0,0.5,s,0.92,0.03,0.04,0.02,0.92,0.03,489.7,0.8,2.4,5.1,1644,2.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0 +34,20240310A,902.5,25,19.1,40.0,601.8,0.2,601.76,0.855,4.19,13.493,0.127,2.23,0.07,-3.23,0.5,846.4,1.3,0.13,s,0.72,0.03,0.09,0.03,0.72,0.03,-1709.2,1.1,-4.8,7.4,VELA,2.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0,9999.0 +35,20240318A,902.5,23,13.2,119.0,256.4,0.3,256.18,0.015,0.286,0.837,9999.0,0.163,0.01,-3.32,0.005,920.5,0.128,0.005,m,0.8,0.02,0.13,0.01,0.81,0.02,-48.5,0.3,14.3,2.3,1644,1.0,0.8,919.5,4.1,0.2,1.0,1.0,4200.0,330.0,9999.0,9999.0 diff --git a/zdm/optical.py b/zdm/optical.py index e5c08af5..0f8a8229 100644 --- a/zdm/optical.py +++ b/zdm/optical.py @@ -899,10 +899,10 @@ def init_path_raw_prior_Oi(self,DM,grid): self.prior_DM = DM self.raw_priors = priors - pUgm = pU_g_mr = pogm(self.AppMags,self.pU_mean,self.pU_width) + pU = pUgm(self.AppMags,self.pU_mean,self.pU_width) - self.priors = self.raw_priors * (1.-pUgm) - self.PU = self.raw_priors * pUgm + self.priors = self.raw_priors * (1.-pU) + self.PU = self.raw_priors * pU # sets the PATH user function to point to its own @@ -1342,10 +1342,10 @@ def plot_frb(name,ralist,declist,plist,opfile): -def pogm(mag,mean,width): +def pUgm(mag,mean,width): """ - Function to describe probability of identifying a galaxy in - an optical image as a function of its magnitude + Function to describe probability of a galaxy being unidentified + in an optical image as a function of its magnitude Args: mag (float or array of floats): magnitude(s) at which @@ -1359,8 +1359,8 @@ def pogm(mag,mean,width): diff = (mag-mean)/width # converts the diff to a power of 10 - pmr = 1.-1./(1+np.exp(diff)) + pU = 1./(1+np.exp(diff)) - return pmr + return pU diff --git a/zdm/optical_numerics.py b/zdm/optical_numerics.py index 7a497c88..5bae73d2 100644 --- a/zdm/optical_numerics.py +++ b/zdm/optical_numerics.py @@ -39,12 +39,9 @@ def function(x,args): # initialises model to the priors # generates one per grid, due to possible different zvals - model.init_args(x) wrappers = make_wrappers(model,gs) - print("calculating arguments ",x) - NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUprior,PUobs,sumPUprior,sumPUobs = calc_path_priors(frblist,ss,gs,wrappers,verbose=False) # we re-normalise the sum of PUs by NFRB @@ -172,16 +169,18 @@ def calc_path_priors(frblist,ss,gs,wrappers,verbose=True,usemodel=True): # calculates unseen prior if usemodel: - PU = wrapper.estimate_unseen_prior() + P_U = wrapper.estimate_unseen_prior() else: - PU = 0.1 + P_U = 0.1 MagPriors[:] = 1./len(MagPriors) # log-uniform priors when no model used + + # sets magnitude priors to zero when they are above the magnitude limit #bad = np.where(AppMags > mag_limit)[0] #MagPriors[bad] = 0. - P_O,P_Ox,P_Ux,ObsMags,ptbl = run_path(frb,usemodel=usemodel,PU = PU) + P_O,P_Ox,P_Ux,ObsMags,ptbl = run_path(frb,usemodel=usemodel,P_U = P_U) if i==0: allgals = ptbl @@ -205,9 +204,9 @@ def calc_path_priors(frblist,ss,gs,wrappers,verbose=True,usemodel=True): allPOx.append(P_Ox) allMagPriors.append(MagPriors) - sumPU += PU + sumPU += P_U sumPUx += P_Ux - allPU.append(PU) + allPU.append(P_U) allPUx.append(P_Ux) @@ -216,7 +215,7 @@ def calc_path_priors(frblist,ss,gs,wrappers,verbose=True,usemodel=True): # saves all galaxies if not os.path.exists("allgalaxies.csv"): subset.to_csv("allgalaxies.csv",index=False) - exit() + return nfitted,AppMags,allMagPriors,allObsMags,allPOx,allPU,allPUx,sumPU,sumPUx @@ -274,7 +273,7 @@ def calculate_ks_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUobs Inputs: AppMags: array listing apparent magnitudes - AppMagPrior: array giving prior on AppMags + AppMagPriors: list of lists giving priors on AppMags for each FRB ObsMags: list of observed magnitudes ObsPosteriors: list of posterior values corresponding to ObsMags PUobs: posterior on unseen probability @@ -285,9 +284,10 @@ def calculate_ks_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUobs Returns: k-like statistic of biggest obs/prior difference """ - - # creates flattened lists - fAppMagPrios = np.array(flatten(AppMagPriors)) + # sums the apparent mag priors over all FRBs to create a cumulative distribution + fAppMagPriors = np.zeros([len(AppMags)]) + for i,amp in enumerate(AppMagPriors): + fAppMagPriors += amp fObsPosteriors = np.array(flatten(ObsPosteriors)) @@ -338,12 +338,15 @@ def calculate_ks_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUobs -def run_path(name,PU=0.1,usemodel = False, sort = False): +def run_path(name,P_U=0.1,usemodel = False, sort=False): """ evaluates PATH on an FRB - absolute [bool]: if True, treats rel_error as an absolute value - in arcseconds + Args: + P_U [float]: unseen prior + usemodel [bool]: if True, use user-defined P_O|x model + sort [bool]: if True, sort candidates by posterior + """ from frb.frb import FRB from astropath.priors import load_std_priors @@ -374,7 +377,7 @@ def run_path(name,PU=0.1,usemodel = False, sort = False): prior['theta'] = theta_new # change this to something depending on the FRB DM - prior['U']=PU + prior['U']=P_U candidates = ptbl[['ang_size', 'mag', 'ra', 'dec', 'separation']] @@ -405,11 +408,11 @@ def run_path(name,PU=0.1,usemodel = False, sort = False): prior['theta']['max'], prior['theta']['scale']) - P_O=this_path.calc_priors() + P_O=this_path.calc_priors() # Calculate p(O_i|x) debug = True - P_Ox,P_U = this_path.calc_posteriors('fixed', + P_Ox,P_Ux = this_path.calc_posteriors('fixed', box_hwidth=10., max_radius=10., debug=debug) @@ -421,5 +424,5 @@ def run_path(name,PU=0.1,usemodel = False, sort = False): P_Ox = P_Ox[indices] mags = mags[indices] - return P_O,P_Ox,P_U,mags,ptbl + return P_O,P_Ox,P_Ux,mags,ptbl diff --git a/zdm/scripts/Path/optimise_host_priors.py b/zdm/scripts/Path/optimise_host_priors.py index e7ebac08..86bb7f43 100644 --- a/zdm/scripts/Path/optimise_host_priors.py +++ b/zdm/scripts/Path/optimise_host_priors.py @@ -63,7 +63,7 @@ def main(): modelname = "loudas" modelname = "simple" - opdir = modelname+"_0.9_output/" + opdir = modelname+"_output/" POxcut = None # set to e.g. 0.9 to reject FRBs with lower posteriors when doing model comparisons @@ -80,11 +80,14 @@ def main(): opstate.simple.k = 1. model = opt.simple_host_model(opstate) x0 = model.get_args() + Nparams = len(x0) + bounds = [(-5,5)]+[(0,1)]*(Nparams-1) elif modelname=="loudas": #### case of Loudas model model = opt.loudas_model() x0 = [0.5] + bounds=[(-3,3)] # large range else: print("Unrecognised host model ", modelname) @@ -92,48 +95,42 @@ def main(): # setting istat=0 means using a ks statistic to fit p(m_r) istat=0 # setting istat=1 means using a maximum likelihood estimator - istat=1 + #istat=1 # initialise aguments to minimisation function args=[frblist,ss,gs,model,POxcut,istat] - Nparams = len(x0) - bounds = [(0,1)]*Nparams # "function" is the function that performs the comparison of # predictions to outcomes. It's where all the magic happens - result = minimize(on.function,x0 = x0,args=args,bounds = bounds) - - # Recording the current spline best-fit here - #x = [0.00000000e+00 0.00000000e+00 7.05155614e-02 8.39235326e-01 - # 3.27794398e-01 1.00182186e-03 0.00000000e+00 3.46702511e-04 - # 2.17040011e-03 9.72472750e-04] - - # recording the current non-spline best fit here - #x = [ 1.707e-04, 8.649e-02, 9.365e-01, 9.996e-01, 2.255e-01,\ - # 3.493e-02, 0.000e+00, 0.000e+00, 0.000e+00, 1.000e-01] - #x = np.array(x) + result = minimize(on.function,x0 = x0,args=args,bounds = bounds) print("Best fit result is ",result.x) x = result.x + # saves result + np.save(opdir+"/best_fit_params.npy",x) # analyses final result if modelname == "simple": # renormalise distribution in parameters x /= np.sum(x) + # initialises arguments model.init_args(x) outfile = opdir+"best_fit_apparent_magnitudes.png" wrappers = on.make_wrappers(model,gs) NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUprior,PUobs,sumPUprior,sumPUobs = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) - stat = on.calculate_goodness_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,sumPUobs,sumPUprior,plotfile=outfile) + stat = on.calculate_ks_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,sumPUobs,sumPUprior,plotfile=outfile) # calculates the original PATH result outfile = opdir+"original_fit_apparent_magnitudes.png" NFRB2,AppMags2,AppMagPriors2,ObsMags2,ObsPosteriors2,PUprior2,PUobs2,sumPUprior2,sumPUobs2 = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False,usemodel=False) - stat = on.calculate_goodness_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,sumPUobs,sumPUprior,plotfile=outfile) + stat = on.calculate_ks_statistic(NFRB,AppMags2,AppMagPriors2,ObsMags2,ObsPosteriors2,sumPUobs2,sumPUprior2,plotfile=outfile) + # flattens lists of lists + ObsPosteriors = [x for xs in ObsPosteriors for x in xs] + ObsPosteriors2 = [x for xs in ObsPosteriors2 for x in xs] # plots original vs updated posteriors plt.figure() @@ -175,7 +172,7 @@ def main(): NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUprior,\ PUobs,sumPUprior,sumPUobs = on.calc_path_priors(frblist, ss,gs,wrappers,verbose=False) - stat = on.calculate_goodness_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,sumPUobs, + stat = on.calculate_likelihood_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,sumPUobs, sumPUprior,plotfile=outfile,POxcut=POxcut) stats[istat] = stat outfile = opdir+"scan_sfr.png" From 851b753118b403afde1761456c40e96e3949f907 Mon Sep 17 00:00:00 2001 From: Clancy James Date: Fri, 6 Feb 2026 15:38:32 +0800 Subject: [PATCH 16/35] updated priorto path debug to check original method --- papers/pathpriors/compare_posteriors.py | 161 +++++++++++ papers/pathpriors/fit_loudas_model.py | 165 +++++++++++ papers/pathpriors/fit_simple_model.py | 209 ++++++++++++++ papers/pathpriors/pU_g_mr/test_pogmr.py | 47 +++- papers/pathpriors/plot_craft_optical_data.py | 17 +- papers/pathpriors/plot_marnoch_model.py | 86 ++++++ zdm/optical.py | 238 +++++++++++++--- zdm/optical_numerics.py | 278 +++++++++++++++---- zdm/optical_params.py | 4 +- zdm/scripts/Path/optimise_host_priors.py | 103 +++---- 10 files changed, 1133 insertions(+), 175 deletions(-) create mode 100644 papers/pathpriors/compare_posteriors.py create mode 100644 papers/pathpriors/fit_loudas_model.py create mode 100644 papers/pathpriors/fit_simple_model.py create mode 100644 papers/pathpriors/plot_marnoch_model.py diff --git a/papers/pathpriors/compare_posteriors.py b/papers/pathpriors/compare_posteriors.py new file mode 100644 index 00000000..adc6cb14 --- /dev/null +++ b/papers/pathpriors/compare_posteriors.py @@ -0,0 +1,161 @@ +""" +This file fits the simple (naive) model to CRAFT ICDS observations. +It fits a model of absolute galaxy magnitude distributions, +uses zDM to predict redshifts and hence apparent magntidues, +runs PATH using that prior, and tries to get priors to match posteriors. + +""" + + +#standard Python imports +import os +import numpy as np +from matplotlib import pyplot as plt +from scipy.optimize import minimize + +# imports from the "FRB" series +from zdm import optical as opt +from zdm import optical_params as op +from zdm import loading +from zdm import cosmology as cos +from zdm import parameters +from zdm import loading +from zdm import optical_numerics as on +from zdm import states + +# other FRB library imports +import astropath.priors as pathpriors + +import matplotlib + +defaultsize=14 +ds=4 +font = {'family' : 'Helvetica', + 'weight' : 'normal', + 'size' : defaultsize} +matplotlib.rc('font', **font) + + +def main(): + """ + Main function + Contains outer loop to iterate over parameters + + """ + + + ######### List of all ICS FRBs for which we can run PATH ####### + # hard-coded list of FRBs with PATH data in ice paper + frblist=opt.frblist + + # Initlisation of zDM grid + # Eventually, this should be part of the loop, i.e. host IDs should + # be re-fed into FRB surveys. However, it will be difficult to do this + # with very limited redshift estimates. That might require posterior + # estimates of redshift given the observed galaxies. Maybe. + state = states.load_state("HoffmannHalo25",scat=None,rep=None) + #state = parameters.State() + cos.set_cosmology(state) + cos.init_dist_measures() + names=['CRAFT_ICS_892','CRAFT_ICS_1300','CRAFT_ICS_1632'] + ss,gs = loading.surveys_and_grids(survey_names=names,init_state=state) + + + # initialise figure + plt.figure() + plt.xlabel("$P(O_i| \\mathbf{x})$ [original; $P(U)=0.1$]") + plt.ylabel("$P(O_i| \\mathbf{x},N_O)$ [this work]") + + + ##### Begins by calculating the original PATH result ##### + # calculates the original PATH result + wrappers = None + NFRB2,AppMags2,AppMagPriors2,ObsMags2,ObsPrior2,ObsPosteriors2,PUprior2,PUobs2,sumPUprior2,sumPUobs2,frbs,dms = \ + on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False,usemodel=False,P_U=0.) + fObsPosteriors2 = on.flatten(ObsPosteriors2) + + with open("posteriors/orig.txt",'w') as f: + for i,frb in enumerate(frbs): + f.write(str(i)+" "+frb+" "+str(dms[i])[0:5]+" "+str(PUprior2[i])[0:4]+"\n") + for j,om in enumerate(ObsMags2[i]): + f.write(str(om)[0:5]+" "+ "%e" % ObsPrior2[i][j]+" "+"%e" % ObsPosteriors2[i][j]+"\n") + f.write("\n") + + + ####### Model 1: Marnoch ######## + + # model 1: Marnoch + model = opt.marnoch_model() + + wrappers = on.make_wrappers(model,gs) + NFRB,AppMags,AppMagPriors,ObsMags,ObsPrior,ObsPosteriors,PUprior,PUobs,sumPUprior,sumPUobs,frbs,dms = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) + + fObsPosteriors = on.flatten(ObsPosteriors) + plt.scatter(fObsPosteriors2,fObsPosteriors,label="Marnoch",marker='s') + + + with open("posteriors/marnoch.txt",'w') as f: + for i,frb in enumerate(frbs): + f.write(str(i)+" "+frb+" "+str(dms[i])[0:5]+" "+str(PUprior[i])[0:4]+"\n") + for j,om in enumerate(ObsMags[i]): + f.write(str(om)[0:5]+" "+ "%e" % ObsPrior[i][j]+" "+"%e" % ObsPosteriors[i][j]+"\n") + f.write("\n") + + ####### Model 2: Loudas ######## + + model = opt.loudas_model() + model.init_args([1.68]) # best-fit arguments + wrappers = on.make_wrappers(model,gs) + NFRB,AppMags,AppMagPriors,ObsMags,ObsPrior,ObsPosteriors,PUprior,PUobs,sumPUprior,sumPUobs,frbs,dms = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) + + fObsPosteriors = on.flatten(ObsPosteriors) + plt.scatter(fObsPosteriors2,fObsPosteriors,label="Loudas",marker='x') + + with open("posteriors/loudas.txt",'w') as f: + for i,frb in enumerate(frbs): + f.write(str(i)+" "+frb+" "+str(dms[i])[0:5]+" "+str(PUprior[i])[0:4]+"\n") + for j,om in enumerate(ObsMags[i]): + f.write(str(om)[0:5]+" "+ "%e" % ObsPrior[i][j]+" "+"%e" % ObsPosteriors[i][j]+"\n") + f.write("\n") + + + ####### Model 3: Simple ######## + + # Case of simple host model + opstate = op.OpticalState() + # sets optical state to use simple linear interpolation + opstate.simple.AbsModelID = 1 # linear interpolation + opstate.simple.AppModelID = 1 # include k-correction + opstate + model = opt.simple_host_model(opstate) + + # retrieve default initial arguments in vector form + x = [-2.29467289 ,0. , 0. , 0. ,0.1109831,0.72688895, 1. , 0. , 0. , 0. , 0.] + + # initialises best-fit arguments + model.init_args(x) + + ############# Generate a KS-like plot showing the best fits #################### + wrappers = on.make_wrappers(model,gs) + NFRB,AppMags,AppMagPriors,ObsMags,ObsPrior,ObsPosteriors,PUprior,PUobs,sumPUprior,sumPUobs,frbs,dms = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) + + fObsPosteriors = on.flatten(ObsPosteriors) + plt.scatter(fObsPosteriors2,fObsPosteriors,label="Naive",marker='o',s=20) + + plt.legend(loc="lower right") + plt.xlim(0,1) + plt.ylim(0,1) + plt.plot([0,1],[0,1],color="black",linestyle=":") + plt.tight_layout() + plt.savefig("pox_comparison.png") + plt.close() + + with open("posteriors/naive.txt",'w') as f: + for i,frb in enumerate(frbs): + f.write(str(i)+" "+frb+" "+str(dms[i])[0:5]+" "+str(PUprior[i])[0:4]+"\n") + for j,om in enumerate(ObsMags[i]): + f.write(str(om)[0:5]+" "+ "%e" % ObsPrior[i][j]+" "+"%e" % ObsPosteriors[i][j]+"\n") + f.write("\n") + + +main() diff --git a/papers/pathpriors/fit_loudas_model.py b/papers/pathpriors/fit_loudas_model.py new file mode 100644 index 00000000..d5cb68de --- /dev/null +++ b/papers/pathpriors/fit_loudas_model.py @@ -0,0 +1,165 @@ +""" +This file fits and generates plots for the model of Loudas et al +""" + + +#standard Python imports +import os +import numpy as np +from matplotlib import pyplot as plt +from scipy.optimize import minimize + +# imports from the "FRB" series +from zdm import optical as opt +from zdm import optical_params as op +from zdm import loading +from zdm import cosmology as cos +from zdm import parameters +from zdm import loading +from zdm import optical_numerics as on +from zdm import states + +# other FRB library imports +import astropath.priors as pathpriors + +import matplotlib + +defaultsize=14 +ds=4 +font = {'family' : 'Helvetica', + 'weight' : 'normal', + 'size' : defaultsize} +matplotlib.rc('font', **font) + + +def main(): + """ + Main function + Contains outer loop to iterate over parameters + + """ + + ######### List of all ICS FRBs for which we can run PATH ####### + # hard-coded list of FRBs with PATH data in ice paper + frblist=opt.frblist + + # Initlisation of zDM grid + # Eventually, this should be part of the loop, i.e. host IDs should + # be re-fed into FRB surveys. However, it will be difficult to do this + # with very limited redshift estimates. That might require posterior + # estimates of redshift given the observed galaxies. Maybe. + state = states.load_state("HoffmannHalo25",scat=None,rep=None) + #state = parameters.State() + cos.set_cosmology(state) + cos.init_dist_measures() + names=['CRAFT_ICS_892','CRAFT_ICS_1300','CRAFT_ICS_1632'] + ss,gs = loading.surveys_and_grids(survey_names=names,init_state=state) + + + ######## Determnine which statistic to use in optimisation ######## + # setting istat=0 means using a ks statistic to fit p(m_r) + # setting istat=1 means using a maximum likelihood estimator + istat=1 + # determines which model to use + modelname = "loudas" + + opdir = modelname+"_output/" + POxcut = None # set to e.g. 0.9 to reject FRBs with lower posteriors when doing model comparisons + + if not os.path.exists(opdir): + os.mkdir(opdir) + + model = opt.loudas_model() + # set f_sfr starting value to 0.5 + x0 = [0.5] + bounds=[(-3,3)] # large range - physical region is 0 to 1 + + # initialise aguments to minimisation function + args=[frblist,ss,gs,model,POxcut,istat] + + # turn minimise on to re-perform the minimusation. But it's already been done + minimise=False + if minimise: + result = minimize(on.function,x0 = x0,args=args,bounds = bounds) + print("Best fit result is f_sfr = ",result.x) + x = result.x + # saves result + np.save(opdir+"/best_fit_params.npy",x) + else: + # replace later + x=[1.68] + print("using previous result of f_sfr = ",x) + + # initialises arguments + model.init_args(x) + + outfile = opdir+"best_fit_apparent_magnitudes.png" + wrappers = on.make_wrappers(model,gs) + NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUprior,PUobs,sumPUprior,sumPUobs = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) + llstat = on.calculate_likelihood_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUobs,PUprior,plotfile=outfile) + + ksstat = on.calculate_ks_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,sumPUobs,sumPUprior,plotfile=outfile) + + print("Best-fit stats of the Loudas model are ll=",llstat," ks = ",ksstat) + + # makes a ks stat plot comparing three scenarios - SFR, M*, and best fit + + NFRBlist = [] + AppMagslist = [] + AppMagPriorslist = [] + ObsMagslist = [] + ObsPosteriorslist = [] + PUpriorlist = [] + PUobslist = [] + sumPUpriorlist = [] + sumPUobslist = [] + + for f_sfr in [1.68,0,1]: + x=[f_sfr] + model.init_args(x) + wrappers = on.make_wrappers(model,gs) + NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUprior,PUobs,sumPUprior,sumPUobs = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) + + NFRBlist.append(NFRB) + AppMagslist.append(AppMags) + AppMagPriorslist.append(AppMagPriors) + ObsMagslist.append(ObsMags) + ObsPosteriorslist.append(ObsPosteriors) + PUpriorlist.append(PUprior) + PUobslist.append(PUobs) + sumPUpriorlist.append(sumPUprior) + sumPUobslist.append(sumPUobs) + + NMODELS = 3 + plotlabels=["$f_{\\rm sfr} = 1.68$","$f_{\\rm sfr} = 0$", "$f_{\\rm sfr} = 1$"] + plotfile = opdir+"loudas_f0_1_best_comparison.png" + on.make_cumulative_plots(NMODELS,NFRBlist,AppMagslist,AppMagPriorslist,ObsMagslist,ObsPosteriorslist, + PUobslist,PUpriorlist,plotfile,plotlabels,POxcut=None,onlyobs=0,abc="(b)") + + + NSFR=31 + stats = np.zeros([NSFR]) + SFRs = np.linspace(0,3,NSFR) + for istat,sfr in enumerate(SFRs): + + model.init_args([sfr]) + wrappers = on.make_wrappers(model,gs) + NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUprior,\ + PUobs,sumPUprior,sumPUobs = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) + stat = on.calculate_likelihood_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUobs, + PUprior,plotfile=outfile,POxcut=POxcut) + stats[istat] = stat + outfile = opdir+"scan_sfr.png" + plt.figure() + plt.plot(SFRs,stats,marker="o") + plt.xlabel("$f_{\\rm sfr}$") + plt.ylabel("$\\log_{10} \\mathcal{L}(f_{\\rm sfr})$") + plt.xlim(0,3) + plt.ylim(46,52) + plt.tight_layout() + plt.savefig(outfile) + plt.close() + + + +main() diff --git a/papers/pathpriors/fit_simple_model.py b/papers/pathpriors/fit_simple_model.py new file mode 100644 index 00000000..3106eaeb --- /dev/null +++ b/papers/pathpriors/fit_simple_model.py @@ -0,0 +1,209 @@ +""" +This file fits the simple (naive) model to CRAFT ICDS observations. +It fits a model of absolute galaxy magnitude distributions, +uses zDM to predict redshifts and hence apparent magntidues, +runs PATH using that prior, and tries to get priors to match posteriors. + +""" + + +#standard Python imports +import os +import numpy as np +from matplotlib import pyplot as plt +from scipy.optimize import minimize + +# imports from the "FRB" series +from zdm import optical as opt +from zdm import optical_params as op +from zdm import loading +from zdm import cosmology as cos +from zdm import parameters +from zdm import loading +from zdm import optical_numerics as on +from zdm import states + +# other FRB library imports +import astropath.priors as pathpriors + +import matplotlib + +defaultsize=14 +ds=4 +font = {'family' : 'Helvetica', + 'weight' : 'normal', + 'size' : defaultsize} +matplotlib.rc('font', **font) + + +def main(): + """ + Main function + Contains outer loop to iterate over parameters + + """ + + ######### List of all ICS FRBs for which we can run PATH ####### + # hard-coded list of FRBs with PATH data in ice paper + frblist=opt.frblist + + # Initlisation of zDM grid + # Eventually, this should be part of the loop, i.e. host IDs should + # be re-fed into FRB surveys. However, it will be difficult to do this + # with very limited redshift estimates. That might require posterior + # estimates of redshift given the observed galaxies. Maybe. + state = states.load_state("HoffmannHalo25",scat=None,rep=None) + #state = parameters.State() + cos.set_cosmology(state) + cos.init_dist_measures() + names=['CRAFT_ICS_892','CRAFT_ICS_1300','CRAFT_ICS_1632'] + ss,gs = loading.surveys_and_grids(survey_names=names,init_state=state) + + + ######## Determnine which statistic to use in optimisation ######## + # setting istat=0 means using a ks statistic to fit p(m_r) + # setting istat=1 means using a maximum likelihood estimator + istat=1 + # dok=True means use the k-correction + dok = True + # we are using the simple model + modelname = "simple" + # set to e.g. 0.9 to reject FRBs with lower posteriors when doing model comparisons + POxcut = None + + opdir = modelname+"_output/" + + + if not os.path.exists(opdir): + os.mkdir(opdir) + + # Case of simple host model + # Initialisation of model + # simple host model + opstate = op.OpticalState() + # sets optical state to use simple linear interpolation + opstate.simple.AbsModelID = 1 + + # sets up initial bounds on variables + if dok: + opstate.simple.AppModelID = 1 # k-correction + Nparams = opstate.simple.NModelBins+1 + opstate.simple.AppModelID = 1 # sets to include k-correction + opstate.simple.k = 0.5 # for some reason, this just doesn't make much difference to results + bounds = [(-25,25)]+[(0,1)]*(Nparams-1) + else: + Nparams = opstate.simple.NModelBins + # bins now give log-space values, hence -5,2 is range of 10^7 + if opstate.simple.AbsModelID == 3: + base=(-5,2) # log space + else: + base=(0,1) # linear space + bounds = [base]*(Nparams) + opstate.simple.AppModelID = 0 # no k-correction + + model = opt.simple_host_model(opstate) + + # retrieve default initial arguments in vector form + x0 = model.get_args() + + # initialise aguments to minimisation function + args=[frblist,ss,gs,model,POxcut,istat] + + # set to false to just use hard-coded best fit parameters + minimise=False + if minimise: + result = minimize(on.function,x0 = x0,args=args,bounds = bounds) + print("Best fit result is ",result.x) + x = result.x + # saves result + np.save(opdir+"/best_fit_params.npy",x) + else: + # hard-coded best fit parameters from running optimisation + if not dok: + x = [0.,0.,0.,0.18691816,0.99999953,0.44064664,0.,0.,0.,0.] + else: + x = [-2.29467289 ,0. , 0. , 0. ,0.1109831,0.72688895, 1. , 0. , 0. , 0. , 0.] + + # initialises best-fit arguments + model.init_args(x) + + ############# Generate a KS-like plot showing the best fits #################### + outfile = opdir+"best_fit_apparent_magnitudes.png" + wrappers = on.make_wrappers(model,gs) + NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUprior,PUobs,sumPUprior,sumPUobs = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) + llstat = on.calculate_likelihood_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUobs,PUprior,plotfile=outfile) + ksstat = on.calculate_ks_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,sumPUobs, + sumPUprior,plotfile=outfile,abc="(c)",tag="naive: ",) + + print("Best-fit stats of the naive model are ll=",llstat," ks = ",ksstat) + + + ############ k-correction figure ############3 + # we generate a plot showing the convergence on k, i.e. how/why we get a best fit + nk=21 + kvals = np.linspace(-5,5,nk) + stats = np.zeros([nk]) + for i,kcorr in enumerate(kvals): + x[0] = kcorr + model.init_args(x) + wrappers = on.make_wrappers(model,gs) + NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUprior,PUobs,sumPUprior,sumPUobs = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) + stat = on.calculate_likelihood_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUobs,PUprior) + stats[i] = stat + + plt.figure() + plt.plot(kvals,stats) + #plt.yscale('log') + plt.xlabel('$k$') + plt.ylabel('$\\log_{10} \\mathcal{L} (k)$') + plt.tight_layout() + plt.savefig('pkvalue.png') + plt.close() + + + # calculates the original PATH result + outfile = opdir+"original_fit_apparent_magnitudes.png" + NFRB2,AppMags2,AppMagPriors2,ObsMags2,ObsPosteriors2,PUprior2,PUobs2,sumPUprior2,sumPUobs2 = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False,usemodel=False) + stat = on.calculate_ks_statistic(NFRB,AppMags2,AppMagPriors2,ObsMags2,ObsPosteriors2,sumPUobs2,sumPUprior2,plotfile=outfile) + + # flattens lists of lists + ObsPosteriors = [x for xs in ObsPosteriors for x in xs] + ObsPosteriors2 = [x for xs in ObsPosteriors2 for x in xs] + + # plots original vs updated posteriors + plt.figure() + plt.xlabel("Original P") + plt.ylabel("Best fit P") + plt.xlim(0,1) + plt.ylim(0,1) + plt.scatter(ObsPosteriors2,ObsPosteriors,label="Hosts",marker='x') + plt.scatter(PUobs2,PUobs,label="Unobserved",marker='+') + plt.legend() + plt.tight_layout() + plt.savefig(opdir+"Scatter_plot_comparison.png") + plt.close() + + ####### Plots that only make sense for specific models ########3 + + plt.figure() + plt.xlabel("Absolute magnitude, $M_r$") + plt.ylabel("$p(M_r)$") + plt.plot(model.AbsMags,model.AbsMagWeights/np.max(model.AbsMagWeights),label="interpolation") + + if dok: + toplot = x[1:] + else: + toplot = x + + if model.AbsModelID == 3: + toplot = 10**toplot + plt.plot(model.ModelBins,toplot/np.max(toplot),marker="o",linestyle="",label="Model Parameters") + plt.legend() + plt.tight_layout() + plt.savefig(opdir+"best_fit_absolute_magnitudes.png") + plt.close() + + + + +main() diff --git a/papers/pathpriors/pU_g_mr/test_pogmr.py b/papers/pathpriors/pU_g_mr/test_pogmr.py index 2ac80e14..5121419f 100644 --- a/papers/pathpriors/pU_g_mr/test_pogmr.py +++ b/papers/pathpriors/pU_g_mr/test_pogmr.py @@ -3,7 +3,7 @@ This script fits data to the p(U|mr) data, i.e. the probability that a galaxy is unseen given it has an intrinsic magnitude of m_r. -The functional form of the fits is given by opt.pogm +The functional form of the fits is given by opt.pUgm The data is provided by Michelle Woodland (for CRAFT) and Bridget Anderson (data to be published) @@ -19,11 +19,23 @@ import pandas as pd from scipy.optimize import curve_fit + +import matplotlib + +defaultsize=14 +ds=4 +font = {'family' : 'Helvetica', + 'weight' : 'normal', + 'size' : defaultsize} +matplotlib.rc('font', **font) + + + # relevant for CRAFT, from Bridget df = pd.read_csv("pu_mr_vs_mag_CRAFT_VLT_FORS2_r.csv") -result = curve_fit(opt.pogm,df['mag'],df['PU_mr'],p0=[26.5,0.3]) -#CRAFT_result = result[0] -CRAFT_pogm = opt.pogm(df['mag'],result[0][0],result[0][1]) +result = curve_fit(opt.pUgm,df['mag'],df['PU_mr'],p0=[26.5,0.3]) +CRAFT_result = result[0] +CRAFT_pogm = opt.pUgm(df['mag'],result[0][0],result[0][1]) # Legacy surveys - from Bridget Anderson Lmags = [11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 20.0, @@ -32,9 +44,9 @@ 0.02436363700867187, 0.014231286256411943, 0.047649506708623335, 0.15510269056554593, 0.4759090774425562, 0.8798642289140987, 1.0, 0.980904733057884] -result = curve_fit(opt.pogm,Lmags,LpU_mr,p0=[26.5,0.3]) +result = curve_fit(opt.pUgm,Lmags,LpU_mr,p0=[26.5,0.3]) Legacy_result = result[0] -Legacy_fit = opt.pogm(Lmags,result[0][0],result[0][1]) +Legacy_fit = opt.pUgm(Lmags,result[0][0],result[0][1]) #Pan-STARRS: from Bridget Anderson PSmags = [11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 20.0, @@ -43,23 +55,28 @@ 0.0753794861936344, 0.17783737932531407, 0.6123903316329317, 0.9170697731444298, 0.9736154053312236, 1.0, 0.9704756326566667, 0.9993072645593615] -result = curve_fit(opt.pogm,PSmags,PSpU_mr,p0=[26.5,0.3]) +result = curve_fit(opt.pUgm,PSmags,PSpU_mr,p0=[26.5,0.3]) PS_result = result[0] -PS_fit = opt.pogm(PSmags,result[0][0],result[0][1]) +PS_fit = opt.pUgm(PSmags,result[0][0],result[0][1]) + +print("Fit results are...") +print(" PanSTARSS: ",PS_result) +print(" Legacy ",Legacy_result) +print(" VLT/FORS2 ",CRAFT_result) plt.figure() -plt.plot(df['mag'],df['PU_mr'],label="CRAFT data") -plt.plot(df['mag'],CRAFT_pogm,label=" fit",linestyle="--",color = plt.gca().lines[-1].get_color()) -plt.plot(Lmags,LpU_mr,label="Legacy data") -plt.plot(Lmags,Legacy_fit,label=" fit",linestyle="--",color = plt.gca().lines[-1].get_color()) -plt.plot(PSmags,PSpU_mr,label="Pan-STARRS data") -plt.plot(PSmags,PS_fit,label=" fit",linestyle="--",color = plt.gca().lines[-1].get_color()) +plt.plot(df['mag'],df['PU_mr'],label="VLT/FORS2") +plt.plot(df['mag'],CRAFT_pogm,label=" (fit)",linestyle="--",color = plt.gca().lines[-1].get_color()) +plt.plot(Lmags,LpU_mr,label="Legacy surveys") +plt.plot(Lmags,Legacy_fit,label=" (fit)",linestyle="--",color = plt.gca().lines[-1].get_color()) +plt.plot(PSmags,PSpU_mr,label="Pan-STARRS") +plt.plot(PSmags,PS_fit,label=" (fit)",linestyle="--",color = plt.gca().lines[-1].get_color()) plt.legend() plt.xlim(15,30) plt.xlabel("$m_r$") plt.ylabel("$p(U|m_r)$") plt.tight_layout() -plt.savefig("pogm.png") +plt.savefig("pUgm.png") plt.close() diff --git a/papers/pathpriors/plot_craft_optical_data.py b/papers/pathpriors/plot_craft_optical_data.py index 1b4bef90..9ec8eca5 100644 --- a/papers/pathpriors/plot_craft_optical_data.py +++ b/papers/pathpriors/plot_craft_optical_data.py @@ -110,7 +110,22 @@ def main(): plt.tight_layout() plt.savefig("Figures/ang_mag.png") plt.close() - + + + + # creates a plot to check the normalisation of driver et al + + plt.figure() + + plt.xlim(10,30) + plt.ylim(-2,6) + + mags = np.linspace(10,30,21) + driver = chance.driver_sigma(mags) + plt.plot(mags,np.log10(driver*3600*3600/2),color="black") + plt.tight_layout() + plt.savefig("driver_test.png") + plt.close() def int_driver(bins): """ diff --git a/papers/pathpriors/plot_marnoch_model.py b/papers/pathpriors/plot_marnoch_model.py new file mode 100644 index 00000000..c5a887e6 --- /dev/null +++ b/papers/pathpriors/plot_marnoch_model.py @@ -0,0 +1,86 @@ +""" +This file compares predictions and outputs for the model of Marnoch et al +""" + + +#standard Python imports +import os +import numpy as np +from matplotlib import pyplot as plt +from scipy.optimize import minimize + +# imports from the "FRB" series +from zdm import optical as opt +from zdm import optical_params as op +from zdm import loading +from zdm import cosmology as cos +from zdm import parameters +from zdm import loading +from zdm import optical_numerics as on +from zdm import states + +# other FRB library imports +import astropath.priors as pathpriors + +import matplotlib + +defaultsize=14 +ds=4 +font = {'family' : 'Helvetica', + 'weight' : 'normal', + 'size' : defaultsize} +matplotlib.rc('font', **font) + + +def main(): + """ + Main function + Contains outer loop to iterate over parameters + + """ + + ######### List of all ICS FRBs for which we can run PATH ####### + # hard-coded list of FRBs with PATH data in ice paper + frblist=opt.frblist + + # Initlisation of zDM grid + # Eventually, this should be part of the loop, i.e. host IDs should + # be re-fed into FRB surveys. However, it will be difficult to do this + # with very limited redshift estimates. That might require posterior + # estimates of redshift given the observed galaxies. Maybe. + state = states.load_state("HoffmannHalo25",scat=None,rep=None) + #state = parameters.State() + cos.set_cosmology(state) + cos.init_dist_measures() + names=['CRAFT_ICS_892','CRAFT_ICS_1300','CRAFT_ICS_1632'] + ss,gs = loading.surveys_and_grids(survey_names=names,init_state=state) + + + ######## Determnine which statistic to use in optimisation ######## + # setting istat=0 means using a ks statistic to fit p(m_r) + # setting istat=1 means using a maximum likelihood estimator + istat=1 + # determines which model to use + modelname = "marnoch" + + opdir = modelname+"_output/" + POxcut = None # set to e.g. 0.9 to reject FRBs with lower posteriors when doing model comparisons + + if not os.path.exists(opdir): + os.mkdir(opdir) + + model = opt.marnoch_model() + + outfile = opdir+"best_fit_apparent_magnitudes.png" + wrappers = on.make_wrappers(model,gs) + NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUprior,PUobs,sumPUprior,sumPUobs = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) + llstat = on.calculate_likelihood_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUobs,PUprior,plotfile=outfile) + + ksstat = on.calculate_ks_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,sumPUobs, + sumPUprior,plotfile=outfile,abc="(a)",tag="Marnoch: ") + + print("Best-fit stats of the Marnoch model are ll=",llstat," ks = ",ksstat) + + + +main() diff --git a/zdm/optical.py b/zdm/optical.py index 0f8a8229..296664fc 100644 --- a/zdm/optical.py +++ b/zdm/optical.py @@ -500,11 +500,13 @@ def __init__(self,OpticalState=None,verbose=False): print("Initialising simple luminosity function") # must take arguments of (absoluteMag,k,z) self.CalcApparentMags = SimpleApparentMags + self.CalcAbsoluteMags = SimpleAbsoluteMags elif self.opstate.AppModelID == 1: if verbose: print("Initialising k-corrected luminosity function") # must take arguments of (absoluteMag,k,z) self.CalcApparentMags = SimplekApparentMags + self.CalcAbsoluteMags = SimplekAbsoluteMags else: raise ValueError("Model ",self.opstate.AppModelID," not implemented") @@ -512,8 +514,14 @@ def __init__(self,OpticalState=None,verbose=False): if verbose: print("Describing absolute mags with N independent bins") elif self.opstate.AbsModelID == 1: + if verbose: + print("Describing absolute mags with linear interpoilation of N points") + elif self.opstate.AbsModelID == 2: if verbose: print("Describing absolute mags with spline interpoilation of N points") + elif self.opstate.AbsModelID == 3: + if verbose: + print("Describing absolute mags with spline interpoilation of N log points") else: raise ValueError("Model ",self.opstate.AbsModelID," not implemented") @@ -632,7 +640,7 @@ def init_model_bins(self): dbin = (self.Absmax - self.Absmin)/ModelNBins ModelBins = np.linspace(self.Absmin+dbin/2.,self.Absmax-dbin/2.,ModelNBins) - elif self.AbsModelID == 1: + else: # bins on edges ModelBins = np.linspace(self.Absmin,self.Absmax,ModelNBins) @@ -662,26 +670,61 @@ def get_pmr_gz(self,mrbins,z): pmr: probability for each of the bins (length: N) """ - # mapping of apparent to absolute magnitude - if self.opstate.AppModelID == 0: - mrvals = self.CalcApparentMags(self.AbsMags,z) # works with scalar z - elif self.opstate.AppModelID == 1: - mrvals = self.CalcApparentMags(self.AbsMags,self.k,z) # works with scalar z - - - # creates weighted histogram of apparent magnitudes, - # using model weights from wmap (which are fixed for all z) - hist,bins = np.histogram(mrvals,weights=self.AbsMagWeights,bins=mrbins) - - #smoothing function - just to flatten the params - NS=10 - smoothf = self.gauss(mrvals[0:NS] - np.average(mrvals[0:NS])) - smoothf /= np.sum(smoothf) - smoothed = np.convolve(hist,smoothf,mode="same") - - #smoothed=hist. Not sure yet if smoothing is the right thing to do! - pmr = smoothed - + old = False + if old: + # mapping of apparent to absolute magnitude + if self.opstate.AppModelID == 0: + mrvals = self.CalcApparentMags(self.AbsMags,z) # works with scalar z + elif self.opstate.AppModelID == 1: + mrvals = self.CalcApparentMags(self.AbsMags,self.k,z) # works with scalar z + + # creates weighted histogram of apparent magnitudes, + # using model weights from wmap (which are fixed for all z) + hist,bins = np.histogram(mrvals,weights=self.AbsMagWeights,bins=mrbins) + + #smoothing function - just to flatten the params + NS=10 + smoothf = self.gauss(mrvals[0:NS] - np.average(mrvals[0:NS])) + smoothf /= np.sum(smoothf) + smoothed = np.convolve(hist,smoothf,mode="same") + + #smoothed=hist. Not sure yet if smoothing is the right thing to do! + pmr = smoothed + else: + # probability density at bin centre + mrbars = (mrbins[:-1] + mrbins[1:])/2. + + # get absolute magnitudes corresponding to these apparent magnitudes + if self.opstate.AppModelID == 0: + Mrvals = self.CalcAbsoluteMags(mrbars,z) # works with scalar z + elif self.opstate.AppModelID == 1: + Mrvals = self.CalcAbsoluteMags(mrbars,self.k,z) # works with scalar z + + # linear interpolation + # note that dMr = dmr, so we just map probability densities + + kmag2s = (Mrvals - self.Absmin)/self.dMag + imag1s = np.floor(kmag2s).astype('int') + kmag2s -= imag1s + kmag1s = 1.-kmag2s + imag2s = imag1s+1 + + # guards against things that are too low + toolow = np.where(imag1s < 0)[0] + imag1s[toolow]=0 + kmag1s[toolow]=1 + imag2s[toolow]=1 + kmag2s[toolow]=0 + + # guards against things that are too high + toohigh = np.where(imag2s >= self.NAbsBins)[0] + imag1s[toohigh]=self.NAbsBins-2 + kmag1s[toohigh]=0 + imag2s[toohigh]=self.NAbsBins-1 + kmag2s[toohigh]=1. + + pmr = self.AbsMagWeights[imag1s] * kmag1s + self.AbsMagWeights[imag2s] * kmag2s + # # NOTE: these should NOT be re-normalised, since the normalisation reflects # true magnitudes which fall off the apparent magnitude histogram. return pmr @@ -691,7 +734,7 @@ def gauss(self,x,mu=0,sigma=0.1): simple Gaussian smoothing function """ return np.exp(-0.5*(x-mu)**2/sigma**2) - + def init_abs_mag_weights(self): """ Assigns a weight to each of the absolute magnitudes @@ -714,6 +757,11 @@ def init_abs_mag_weights(self): weights = self.AbsPrior[self.imags] elif self.AbsModelID == 1: + # linear interpolation + # gives mapping from model bins to mag bins + weights = np.interp(self.AbsMags,self.ModelBins,self.AbsPrior) + + elif self.AbsModelID == 2: # As above, but with spline interpolation of model. # coefficients span full range cs = CubicSpline(self.ModelBins,self.AbsPrior) @@ -730,12 +778,16 @@ def init_abs_mag_weights(self): if iLastNonzero < self.AbsPrior.size - 1: toohigh = np.where(self.AbsMags > self.ModelBins[iLastNonzero+1]) weights[toohigh] = 0. + elif self.AbsModelID == 3: + # As above, but splines interpolate in *log* space + cs = CubicSpline(self.ModelBins,self.AbsPrior) + weights = cs(self.AbsMags) + weights = 10**weights else: raise ValueError("This weighting scheme not yet implemented") - # renormalises the weights, so all internal apparent mags sum to unit self.AbsMagWeights = weights / np.sum(weights) @@ -850,13 +902,15 @@ def init_zmapping(self,zvals): for i,z in enumerate(zvals): # use the model to calculate p(mr|z) for range of z-values - # this is then stored in an array - p_mr_z[:,i] = self.model.get_pmr_gz(self.AppBins,z) - + # this is then stored in an array. + # NOTE! This could become un-normalised due to + # interpolation falling off the edge + # hence, we normalise it + this_p_mr_z = self.model.get_pmr_gz(self.AppBins,z) + this_p_mr_z /= np.sum(this_p_mr_z) + p_mr_z[:,i] = this_p_mr_z self.p_mr_z = p_mr_z - - # records that this has been initialised self.ZMAP = True @@ -902,8 +956,8 @@ def init_path_raw_prior_Oi(self,DM,grid): pU = pUgm(self.AppMags,self.pU_mean,self.pU_width) self.priors = self.raw_priors * (1.-pU) - self.PU = self.raw_priors * pU - + self.PUdist = self.raw_priors * pU + self.PU = np.sum(self.PUdist) # sets the PATH user function to point to its own pathpriors.USR_raw_prior_Oi = self.path_raw_prior_Oi @@ -973,9 +1027,9 @@ def estimate_unseen_prior(self): #PU = np.sum(self.priors[invisible]) # we now pre-calculate this at the init raw path prior stage - PU = np.sum(self.PU) + #PU = np.sum(self.PU) - return PU + return self.PU def path_raw_prior_Oi(self,mags,ang_sizes,Sigma_ms): """ @@ -1012,6 +1066,9 @@ def path_raw_prior_Oi(self,mags,ang_sizes,Sigma_ms): # calculate the bins in apparent magnitude prior kmag2 = (mag - self.Appmin)/self.dAppmag imag1 = int(np.floor(kmag2)) + imag2 = imag1 + 1 + kmag2 -= imag1 #residual; float + kmag1 = 1.-kmag2 # careful with interpolation - priors are for magnitude bins # with bin edges give by Appmin + N dAppmag. @@ -1023,8 +1080,13 @@ def path_raw_prior_Oi(self,mags,ang_sizes,Sigma_ms): #imag2 = imag1+1 #prior = kmag1*self.priors[imag1] + kmag2*self.priors[imag2] - # very simple - just gives probability for bin it's in - Oi = self.priors[imag1] + # simple linear interpolation + Oi = self.priors[imag1] * kmag1 + self.priors[imag2] * kmag2 + + # correct normalisation - otherwise, priors are defined to sum + # such that \sum priors = 1; here, we need \int priors dm = 1 + Oi /= self.dAppmag + Oi /= Sigma_ms[i] # normalise by host counts Ois.append(Oi) @@ -1109,10 +1171,10 @@ def SimplekApparentMags(Abs,k,zs): # k-corrections kcorrs = (1+zs)**k - # relative magnitude - dMag = 2.5*np.log10((lds/dabs)**(2)) + 2.5*np.log10(kcorrs) - + dk = 2.5*np.log10(kcorrs) #i.e., 2.5*k*np.log10(1+z) + # relative magnitude + dMag = 2.5*np.log10((lds/dabs)**(2)) + dk if np.isscalar(zs) or np.isscalar(Abs): @@ -1130,9 +1192,10 @@ def SimplekApparentMags(Abs,k,zs): ApparentMags = np.log10(ApparentMags) return ApparentMags -def SimpleApparentMags(Abs,zs): +def SimplekAbsoluteMags(App,k,zs): """ - Function to convert galaxy absolue to apparent magnitudes. + Function to convert galaxy apparent to absolute magnitudes. + Same as simple absolute mags mags, but allows for a k-correction. Nominally, magnitudes are r-band magnitudes, but this function is so simple it doesn't matter. @@ -1140,11 +1203,12 @@ def SimpleApparentMags(Abs,zs): Just applies a distance correction - no k-correction. Args: - Abs (float or array of floats): intrinsic galaxy luminosities + App (float or array of floats): apparent galaxy luminosities + k (float): k-correction zs (float or array of floats): redshifts of galaxies Returns: - ApparentMags: NAbs x NZ array of magnitudes, where these + AbsoluteMags: NAbs x NZ array of magnitudes, where these are the dimensions of the inputs """ @@ -1154,9 +1218,97 @@ def SimpleApparentMags(Abs,zs): # finds distance relative to absolute magnitude distance dabs = 1e-5 # in units of Mpc + # k-corrections + kcorrs = (1+zs)**k + + dk = 2.5*np.log10(kcorrs) + # relative magnitude - dMag = 2.5*np.log10((lds/dabs)**2) + dMag = 2.5*np.log10((lds/dabs)**(2)) + dk + + if np.isscalar(zs) or np.isscalar(Abs): + # just return the product, be it scalar x scalar, + # scalar x array, or array x scalar + # this also ensures that the dimensions are as expected + + AbsoluteMags = App - dMag + else: + # Convert to multiplication so we can use + # numpy.outer + temp1 = 10**App + temp2 = 10**-dMag + AbsoluteMags = np.outer(temp1,temp2) + AbsoluteMags = np.log10(ApparentMags) + return AbsoluteMags + +def SimpleAbsoluteMags(App,zs): + """ + Function to convert galaxy apparent to absolute magnitudes. + + Nominally, magnitudes are r-band magnitudes, but this function + is so simple it doesn't matter. + + Just applies a distance correction - no k-correction. + + Args: + App (float or array of floats): apparent galaxy luminosities + zs (float or array of floats): redshifts of galaxies + + Returns: + AbsoluteMags: NAbs x NZ array of magnitudes, where these + are the dimensions of the inputs + """ + + # calculates luminosity distances (Mpc) + lds = cos.dl(zs) + + # finds distance relative to absolute magnitude distance + dabs = 1e-5 # in units of Mpc + + # relative magnitude + dMag = 2.5*np.log10((lds/dabs)**(2)) + + + if np.isscalar(zs) or np.isscalar(Abs): + # just return the product, be it scalar x scalar, + # scalar x array, or array x scalar + # this also ensures that the dimensions are as expected + + AbsoluteMags = App - dMag + else: + # Convert to multiplication so we can use + # numpy.outer + temp1 = 10**App + temp2 = 10**-dMag + AbsoluteMags = np.outer(temp1,temp2) + AbsoluteMags = np.log10(ApparentMags) + return AbsoluteMags + +def SimpleApparentMags(Abs,zs): + """ + Function to convert galaxy absolue to apparent magnitudes. + + Nominally, magnitudes are r-band magnitudes, but this function + is so simple it doesn't matter. + + Just applies a distance correction - no k-correction. + + Args: + Abs (float or array of floats): intrinsic galaxy luminosities + zs (float or array of floats): redshifts of galaxies + + Returns: + ApparentMags: NAbs x NZ array of magnitudes, where these + are the dimensions of the inputs + """ + + # calculates luminosity distances (Mpc) + lds = cos.dl(zs) + lds_pc = lds*1e6 + + # relative magnitude + dMag = 5*np.log10(lds_pc) - 5 if np.isscalar(zs) or np.isscalar(Abs): # just return the product, be it scalar x scalar, @@ -1356,7 +1508,7 @@ def pUgm(mag,mean,width): """ # converts to a number relative to the mean. Will be weird for mags < 0. - diff = (mag-mean)/width + diff = (mean-mag)/width # converts the diff to a power of 10 pU = 1./(1+np.exp(diff)) diff --git a/zdm/optical_numerics.py b/zdm/optical_numerics.py index 5bae73d2..33ed00f9 100644 --- a/zdm/optical_numerics.py +++ b/zdm/optical_numerics.py @@ -52,8 +52,10 @@ def function(x,args): sumPUobs,sumPUprior,plotfile=None,POxcut=POxcut) elif istat==1: stat = calculate_likelihood_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors, - sumPUobs,sumPUprior,plotfile=None,POxcut=POxcut) - + PUobs,PUprior,plotfile=None,POxcut=POxcut) + # need to construct stat so that small values are good! Log-likelihood being good means large! + stat *= -1 + return stat def make_wrappers(model,grids): @@ -88,7 +90,7 @@ def make_cdf(xs,ys,ws,norm = True): return cdf -def calc_path_priors(frblist,ss,gs,wrappers,verbose=True,usemodel=True): +def calc_path_priors(frblist,ss,gs,wrappers,verbose=True,usemodel=True,P_U=0.1): """ Inner loop. Gets passed model parameters, but assumes everything is initialsied from there. @@ -120,6 +122,7 @@ def calc_path_priors(frblist,ss,gs,wrappers,verbose=True,usemodel=True): # new version recording one list per FRB. For max likelihood functionality allObsMags = [] allPOx = [] + allPO = [] allMagPriors = [] sumPU = 0. @@ -128,6 +131,9 @@ def calc_path_priors(frblist,ss,gs,wrappers,verbose=True,usemodel=True): allPUx = [] nfitted = 0 + frbs=[] + dms=[] + for i,frb in enumerate(frblist): # interates over the FRBs. "Do FRB" # P_O is the prior for each galaxy @@ -143,45 +149,60 @@ def calc_path_priors(frblist,ss,gs,wrappers,verbose=True,usemodel=True): # this is the survey to be used g=gs[j] s = ss[j] - wrapper = wrappers[j] + if usemodel: + wrapper = wrappers[j] + jmatch = j + frbs.append(frb) break - if imatch is None: - if verbose: - print("Could not find ",frb," in any survey") - continue + if imatch is None: + if verbose: + print("Could not find ",frb," in any survey") + continue nfitted += 1 - AppMags = wrapper.AppMags + if usemodel: + AppMags = wrapper.AppMags + else: + AppMags = None + # record this info DMEG = s.DMEGs[imatch] - # this is where the particular survey comes into it - - # Must be priors on magnitudes for this FRB - wrapper.init_path_raw_prior_Oi(DMEG,g) + dms.append(DMEG) - # extracts priors as function of absolute magnitude for this grid and DMEG - MagPriors = wrapper.priors + if usemodel: + + # this is where the particular survey comes into it + # Must be priors on magnitudes for this FRB + wrapper.init_path_raw_prior_Oi(DMEG,g) + # extracts priors as function of absolute magnitude for this grid and DMEG + MagPriors = wrapper.priors + else: + MagPriors = None + # defunct now #mag_limit=26 # might not be correct. TODO! Should be in FRB object # calculates unseen prior if usemodel: P_U = wrapper.estimate_unseen_prior() - else: - P_U = 0.1 - MagPriors[:] = 1./len(MagPriors) # log-uniform priors when no model used + #MagPriors[:] = 1./len(MagPriors) # log-uniform priors when no model used - - # sets magnitude priors to zero when they are above the magnitude limit - #bad = np.where(AppMags > mag_limit)[0] - #MagPriors[bad] = 0. - P_O,P_Ox,P_Ux,ObsMags,ptbl = run_path(frb,usemodel=usemodel,P_U = P_U) + # kept here for debugging + if False: + print("P_U is ",P_U) + print("P_O is ",P_O) + print("P_Ox is ",P_Ox) + plt.figure() + plt.plot(AppMags,MagPriors) + plt.show() + plt.close() + if i==0: allgals = ptbl else: @@ -189,19 +210,10 @@ def calc_path_priors(frblist,ss,gs,wrappers,verbose=True,usemodel=True): ObsMags = np.array(ObsMags) - # old version creating a 1D list - #if allObsMags is None: - # allObsMags = ObsMags - # allPOx = P_Ox - # allMagPriors = MagPriors - #else: - # allObsMags = np.append(allObsMags,ObsMags) - # allPOx = np.append(allPOx,P_Ox) - # allMagPriors += MagPriors - # new version creating a list of lists allObsMags.append(ObsMags) allPOx.append(P_Ox) + allPO.append(P_O) allMagPriors.append(MagPriors) sumPU += P_U @@ -209,49 +221,67 @@ def calc_path_priors(frblist,ss,gs,wrappers,verbose=True,usemodel=True): allPU.append(P_U) allPUx.append(P_Ux) - subset = allgals[['frb','mag','VLT_FORS2_R']].copy() # saves all galaxies if not os.path.exists("allgalaxies.csv"): subset.to_csv("allgalaxies.csv",index=False) - return nfitted,AppMags,allMagPriors,allObsMags,allPOx,allPU,allPUx,sumPU,sumPUx + return nfitted,AppMags,allMagPriors,allObsMags,allPO,allPOx,allPU,allPUx,sumPU,sumPUx,frbs,dms def calculate_likelihood_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUobs, PUprior,plotfile=None,POxcut=None): """ Calculates a likelihood for each of the FRBs, and returns the log-likelihood. - We must set each AppMagPriors to 1.-PUprior at the limiting magnitude for each observation, - and sum the ObsPosteriors to be equal to 1.-PUobs at that magnitude. - Then these are what gets summed. - This can be readily done by combining all ObsMags and ObsPosteriors into a single long list, - since this should already be correctly normalised. Priors require their own weight. + The inputs are in two categories. One is a form of lists of lists, where there is one list for + each FRB, and one entry in that list for each host galaxy candidate. Size is NFRB x NCAND + + The other input is where the length of the list matches the internal array size used to + calculate priors on host magnitudes. Size is either NMAG or NFRBxNMAG Inputs: - AppMags: array listing apparent magnitudes - AppMagPrior: array giving prior on AppMags - ObsMags: list of observed magnitudes - ObsPosteriors: list of posterior values corresponding to ObsMags - PUobs: posterior on unseen probability - PUprior: prior on PU + AppMags [array of floats: NMAG]: array listing apparent magnitudes used to calculate priors + AppMagPrior [array of floats NFRB xNMAG]: array giving prior on AppMags + ObsMags: list of lists of floats giving observed magnitudes m_r of host candidates + ObsPosteriors: list of lists float of posterior values P(O|x) corresponding to ObsMags + PUobs [float]: posterior on unseen probability + PUprior [float]: prior on PU plotfile: set to name of output file for comparison plot POxcut: if not None, cut data to fixed POx. Used to simulate current techniques Returns: - k-like statistic of biggest obs/prior difference + log likelihood of the observation """ - # calculates log-likelihood of observation stat=0 + for i in np.arange(NFRB): # sums the likelihoods over each galaxy: p(xi|oi)*p(oi)/Pfield - sumpost = np.sum(ObsPosteriors[i]) + + # calculate the factor by which the p...|x probabilities have been rescaled. + # allows us to undo this effect + rescale = PUobs[i]/PUprior[i] + # the problem is that the posteriors have been rescaled by some factor + # we do not want this! Hence, we work out the rescale factor by comparing + # the rescale on the unseen prior. Then we undo this factor + # (Note: PUobs / rescale = PUprior, hence must divide) + sumpost = np.sum(ObsPosteriors[i])/rescale+PUprior[i] + + if False: + plt.figure() + plt.plot(AppMags,AppMagPriors[i]/np.max(AppMagPriors[i]),label="priors from model") + for j,mag in enumerate(ObsMags[i]): + plt.scatter(ObsMags[i],ObsPosteriors[i],label="posteriors") + + print("Sum gives ",sumpost, " of which PU is ",PUprior[i]) + plt.show() + plt.close() ll = np.log10(sumpost) stat += ll + return stat def flatten(xss): @@ -261,7 +291,7 @@ def flatten(xss): return [x for xs in xss for x in xs] def calculate_ks_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUobs, - PUprior,plotfile=None,POxcut=None): + PUprior,plotfile=None,POxcut=None,plotlabel=None,abc=None,tag=""): """ Calculates a ks-like statistic to be proxy for goodness-of-fit We must set each AppMagPriors to 1.-PUprior at the limiting magnitude for each observation, @@ -280,15 +310,19 @@ def calculate_ks_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUobs PUprior: prior on PU plotfile: set to name of output file for comparison plot POxcut: if not None, cut data to fixed POx. Used to simulate current techniques + abc [None]: add label, e.g. (a), to upper left + tag [string]: string to prefix labels Returns: k-like statistic of biggest obs/prior difference """ # sums the apparent mag priors over all FRBs to create a cumulative distribution fAppMagPriors = np.zeros([len(AppMags)]) + for i,amp in enumerate(AppMagPriors): fAppMagPriors += amp + fObsPosteriors = np.array(flatten(ObsPosteriors)) fObsMags = np.array(flatten(ObsMags)) @@ -325,10 +359,28 @@ def calculate_ks_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUobs plt.figure() plt.xlabel("Apparent magnitude $m_r$") plt.ylabel("Cumulative host galaxy distribution") + plt.ylim(0,1) + + # calcs lowest x that is essentially at max + ixmax = np.where(prior_dist > prior_dist[-1]*0.999)[0][0] + # rounds it up to multiple of 5 + xmax = 5 * (int(AppMags[ixmax]/5.)+1) + ixmin = np.where(prior_dist < 0.01)[0][-1] + xmin = 5*(int(AppMags[ixmin]/5.)) + plt.xlim(xmin,xmax) + #cx,cy = make_cdf_for_plotting(ObsMags,weights=ObsPosteriors) - plt.plot(AppMags,obs_dist,label="Observed") - plt.plot(AppMags,prior_dist,label="Prior") + plt.plot(AppMags,obs_dist,label=tag+"Observed",color="black") + plt.plot(AppMags,prior_dist,label=tag+"Prior",linestyle=":") plt.legend() + + # adds label to plot + if plotlabel is not None: + plt.text((xmin+xmax)/2.,0.05,plotlabel) + + if abc is not None: + plt.text(0.02,0.9,abc,fontsize=16, transform=plt.gcf().transFigure) + plt.tight_layout() plt.savefig(plotfile) plt.close() @@ -336,7 +388,117 @@ def calculate_ks_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUobs return stat - +def make_cumulative_plots(NMODELS,NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUobs, + PUprior,plotfile,plotlabel,POxcut=None,abc=None,onlyobs=None): + """ + Creates cumulative plots of KS-like behaviour for multiple fit outcomes + + Inputs: see "calculate_ks_statistic" except: + - NMODELS (int): number of models to plot + - abc remains unchanged + - NFRB remains unchanged + - plotfile remains unchanged + - onlyobs (int): if not None, only plot observed distribution for this case + - all other parameters have a leading dimension of NMODELS + + Inputs from "calculate_ks_statistic" with extra NMODELS dimension: + AppMags: array listing apparent magnitudes + AppMagPriors: list of lists giving priors on AppMags for each FRB + ObsMags: list of observed magnitudes + ObsPosteriors: list of posterior values corresponding to ObsMags + PUobs: posterior on unseen probability + PUprior: prior on PU + POxcut: if not None, cut data to fixed POx. Used to simulate current techniques + + Returns: + None + """ + + # arrays to hold created observed and prior distributions + prior_dists = [] + obs_dists = [] + + # loops over models to create prior distributions + for imodel in np.arange(NMODELS): + # sums the apparent mag priors over all FRBs to create a cumulative distribution + fAppMagPriors = np.zeros([len(AppMags[imodel])]) + + for i,amp in enumerate(AppMagPriors[imodel]): + fAppMagPriors += amp + + fObsPosteriors = np.array(flatten(ObsPosteriors[imodel])) + + fObsMags = np.array(flatten(ObsMags[imodel])) + + # we calculate a probability using a cumulative distribution + prior_dist = np.cumsum(fAppMagPriors) + + if POxcut is not None: + # cuts data to "good" FRBs only + OK = np.where(fObsPosteriors > POxcut)[0] + Ndata = len(OK) + fObsMags = fObsMags[OK] + fObsPosteriors = np.full([Ndata],1.) # effectively sets these to unity + + + # makes a cdf in units of AppMags, with observations ObsMags weighted by ObsPosteriors + obs_dist = make_cdf(AppMags[imodel],fObsMags,fObsPosteriors,norm=False) + + if POxcut is not None: + # current techniques just assume we have the full distribution + obs_dist /= obs_dist[-1] + prior_dist /= prior_dist[-1] + else: + # the above is normalised to NFRB. We now divide it by this + # might want to be careful here, and preserve this normalisation + obs_dist /= NFRB[imodel] + prior_dist /= NFRB[imodel] #((NFRB-PUprior)/NFRB) / prior_dist[-1] + + # we calculate something like the k-statistic. Includes NFRB normalisation + diff = obs_dist - prior_dist + stat = np.max(np.abs(diff)) + + obs_dists.append(obs_dist) + prior_dists.append(prior_dist) + + # plotting! + plt.figure() + plt.xlabel("Apparent magnitude $m_r$") + plt.ylabel("Cumulative host galaxy distribution") + plt.ylim(0,1) + + for imodel in np.arange(NMODELS): + + # calcs lowest x that is essentially at max + ixmax = np.where(prior_dist > prior_dist[-1]*0.999)[0][0] + # rounds it up to multiple of 5 + xmax = 5 * (int(AppMags[imodel][ixmax]/5.)+1) + ixmin = np.where(prior_dist < 0.001)[0][-1] + xmin = 5*(int(AppMags[imodel][ixmin]/5.)) + + # sets this for each one - yes, it's random which is which, oh well! + plt.xlim(xmin,xmax) + + #cx,cy = make_cdf_for_plotting(ObsMags,weights=ObsPosteriors) + plt.plot(AppMags[imodel],prior_dists[imodel],label=plotlabel[imodel]+": Prior", + linestyle=":") + if onlyobs is None or onlyobs == imodel: + if onlyobs is not None: + color='black' + else: + color=plt.gca().lines[-1].get_color() + plt.plot(AppMags[imodel],obs_dists[imodel],label=plotlabel[imodel]+": Observed", + color=color) + + + if abc is not None: + plt.text(0.02,0.9,abc,fontsize=16, transform=plt.gcf().transFigure) + plt.legend(loc="upper left") + plt.tight_layout() + plt.savefig(plotfile) + plt.close() + + return None def run_path(name,P_U=0.1,usemodel = False, sort=False): """ @@ -364,6 +526,11 @@ def run_path(name,P_U=0.1,usemodel = False, sort=False): pfile = os.path.join(ppath, f'{my_frb.frb_name}_PATH.csv') ptbl = pandas.read_csv(pfile) + + if name=="FRB20181112A": + print(ptbl) + exit() + ngal = len(ptbl) ptbl["frb"] = np.full([ngal],name) @@ -388,8 +555,8 @@ def run_path(name,P_U=0.1,usemodel = False, sort=False): mag=candidates.mag.values) this_path.frb = my_frb - frb_eellipse = dict(a=my_frb.sig_a, - b=my_frb.sig_b, + frb_eellipse = dict(a=np.abs(my_frb.sig_a), + b=np.abs(my_frb.sig_b), theta=my_frb.eellipse['theta']) this_path.init_localization('eellipse', @@ -416,6 +583,7 @@ def run_path(name,P_U=0.1,usemodel = False, sort=False): box_hwidth=10., max_radius=10., debug=debug) + mags = candidates['mag'] if sort: diff --git a/zdm/optical_params.py b/zdm/optical_params.py index e5ca6fd8..74b02e4d 100644 --- a/zdm/optical_params.py +++ b/zdm/optical_params.py @@ -30,7 +30,7 @@ class SimpleParams(data_class.myDataClass): 'Notation': '', }) Absmax: float = field( - default=0., + default=-10., metadata={'help': "Maximum host absolute magnitude", 'unit': 'M_r^{max}', 'Notation': '', @@ -61,7 +61,7 @@ class SimpleParams(data_class.myDataClass): }) AbsModelID: int = field( default=1, - metadata={'help': "Model for describing absolute magnitudes. 0: Simple histogram of absolute magnitudes. 1: spline interpolation of histogram.", + metadata={'help': "Model for describing absolute magnitudes. 0: Simple histogram of absolute magnitudes. 1: linear interpolation, 2: spline interpolation of histogram, 3: spline interpolation in log space", 'unit': '', 'Notation': '', }) diff --git a/zdm/scripts/Path/optimise_host_priors.py b/zdm/scripts/Path/optimise_host_priors.py index 86bb7f43..66b1a67d 100644 --- a/zdm/scripts/Path/optimise_host_priors.py +++ b/zdm/scripts/Path/optimise_host_priors.py @@ -36,6 +36,15 @@ # other FRB library imports import astropath.priors as pathpriors +import matplotlib + +defaultsize=14 +ds=4 +font = {'family' : 'Helvetica', + 'weight' : 'normal', + 'size' : defaultsize} +matplotlib.rc('font', **font) + def main(): """ @@ -54,20 +63,28 @@ def main(): # with very limited redshift estimates. That might require posterior # estimates of redshift given the observed galaxies. Maybe. state = states.load_state("HoffmannHalo25",scat=None,rep=None) - #state = parameters.State() + cos.set_cosmology(state) cos.init_dist_measures() + + # loads zDM grids names=['CRAFT_ICS_892','CRAFT_ICS_1300','CRAFT_ICS_1632'] ss,gs = loading.surveys_and_grids(survey_names=names,init_state=state) - modelname = "loudas" + + ######## Determnine which statistic to use in optimisation ######## + # setting istat=0 means using a ks statistic to fit p(m_r) + # setting istat=1 means using a maximum likelihood estimator + istat=1 + dok = True # turn on k-correction or not + + # determines which model to use + #modelname = "loudas" modelname = "simple" opdir = modelname+"_output/" POxcut = None # set to e.g. 0.9 to reject FRBs with lower posteriors when doing model comparisons - - if not os.path.exists(opdir): os.mkdir(opdir) @@ -76,12 +93,25 @@ def main(): # simple host model if modelname=="simple": opstate = op.OpticalState() - opstate.simple.AppModelID = 1 # sets to include k-correction - opstate.simple.k = 1. + + if dok: + Nparams = opstate.simple.NModelBins+1 + opstate.simple.AppModelID = 1 # sets to include k-correction + opstate.simple.k = 0.5 # for some reason, this just doesn't make much difference to results + bounds = [(-25,25)]+[(0,1)]*(Nparams-1) + else: + Nparams = opstate.simple.NModelBins + # bins now give log-space values, hence -5,2 is range of 10^7 + if opstate.simple.AbsModelID == 3: + base=(-5,2) # log space + else: + base=(0,1) # linear space + bounds = [base]*(Nparams) + opstate.simple.AppModelID = 0 # no k-correction + model = opt.simple_host_model(opstate) x0 = model.get_args() - Nparams = len(x0) - bounds = [(-5,5)]+[(0,1)]*(Nparams-1) + elif modelname=="loudas": #### case of Loudas model @@ -92,11 +122,6 @@ def main(): print("Unrecognised host model ", modelname) - # setting istat=0 means using a ks statistic to fit p(m_r) - istat=0 - # setting istat=1 means using a maximum likelihood estimator - #istat=1 - # initialise aguments to minimisation function args=[frblist,ss,gs,model,POxcut,istat] @@ -109,20 +134,20 @@ def main(): # saves result np.save(opdir+"/best_fit_params.npy",x) - # analyses final result - if modelname == "simple": - # renormalise distribution in parameters - x /= np.sum(x) - - # initialises arguments model.init_args(x) outfile = opdir+"best_fit_apparent_magnitudes.png" wrappers = on.make_wrappers(model,gs) NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUprior,PUobs,sumPUprior,sumPUobs = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) + + # calculates a maximum-likelihood statistic + stat = on.calculate_likelihood_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUobs,PUprior,plotfile=outfile) + + # calculates a KS-like statistic stat = on.calculate_ks_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,sumPUobs,sumPUprior,plotfile=outfile) + # calculates the original PATH result outfile = opdir+"original_fit_apparent_magnitudes.png" NFRB2,AppMags2,AppMagPriors2,ObsMags2,ObsPosteriors2,PUprior2,PUobs2,sumPUprior2,sumPUobs2 = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False,usemodel=False) @@ -145,46 +170,6 @@ def main(): plt.savefig(opdir+"Scatter_plot_comparison.png") plt.close() - ####### Plots that only make sense for specific models ########3 - - if modelname == "simple": - # plots final result on absolute magnitudes - plt.figure() - plt.xlabel("Absolute magnitude, $M_r$") - plt.ylabel("$p(M_r)$") - plt.plot(model.AbsMags,model.AbsMagWeights/np.max(model.AbsMagWeights),label="interpolation") - plt.plot(model.ModelBins,x[1:]/np.max(x[1:]),marker="o",linestyle="",label="Model Parameters") - plt.legend() - plt.tight_layout() - plt.savefig(opdir+"best_fit_absolute_magnitudes.pdf") - plt.close() - - if modelname == "loudas": - - NSFR=41 - stats = np.zeros([NSFR]) - SFRs = np.linspace(0,4,NSFR) - for istat,sfr in enumerate(SFRs): - outfile = opdir+"ks_test_sfr_"+str(sfr)+".png" - #outfile = None - model.init_args(sfr) - wrappers = on.make_wrappers(model,gs) - NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUprior,\ - PUobs,sumPUprior,sumPUobs = on.calc_path_priors(frblist, - ss,gs,wrappers,verbose=False) - stat = on.calculate_likelihood_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,sumPUobs, - sumPUprior,plotfile=outfile,POxcut=POxcut) - stats[istat] = stat - outfile = opdir+"scan_sfr.png" - plt.figure() - plt.plot(SFRs,stats,marker="o") - plt.xlabel("$f_{\\rm sfr}$") - plt.ylabel("ks statistic (lower is better)") - plt.tight_layout() - plt.savefig(outfile) - plt.close() - - def make_cdf_for_plotting(xvals,weights=None): """ From 21d12939f8ba16cfc1254a52fdd8815bf940a95f Mon Sep 17 00:00:00 2001 From: Clancy James Date: Thu, 12 Feb 2026 10:20:27 +0800 Subject: [PATCH 17/35] Fixed error in survey.py reading in parameter values. Updated several scripts for CASATTA, LSST, and CRACO works --- .../ImprovedSurveys/CRAFT_CRACO_900_imp1.ecsv | 2 +- .../CRAFT_CRACO_900_imp_all.ecsv | 2 +- .../CRAFT_CRACO_900_3ms_alldm_zDM.pdf | Bin 75764 -> 0 bytes .../TestSurveys/CRAFT_CRACO_900_3ms_zDM.pdf | Bin 75764 -> 0 bytes papers/CRACO/plot_900_improvements.py | 2 +- papers/CRACO/plot_askap_2030.py | 261 ++++++++++++++++++ papers/Casatta/plot_casatta.py | 12 +- papers/Casatta/sim_casatta.py | 48 ++-- papers/pathpriors/compare_posteriors.py | 79 ++++-- papers/pathpriors/fit_loudas_model.py | 22 +- papers/pathpriors/fit_simple_model.py | 24 +- .../pU_g_mr/{test_pogmr.py => fit_pogmr.py} | 0 papers/pathpriors/plot_marnoch_model.py | 5 +- zdm/optical.py | 44 ++- zdm/optical_numerics.py | 66 ++++- zdm/states.py | 2 +- zdm/survey.py | 18 +- 17 files changed, 496 insertions(+), 91 deletions(-) delete mode 100644 papers/CRACO/TestSurveys/CRAFT_CRACO_900_3ms_alldm_zDM.pdf delete mode 100644 papers/CRACO/TestSurveys/CRAFT_CRACO_900_3ms_zDM.pdf create mode 100644 papers/CRACO/plot_askap_2030.py rename papers/pathpriors/pU_g_mr/{test_pogmr.py => fit_pogmr.py} (100%) diff --git a/papers/CRACO/ImprovedSurveys/CRAFT_CRACO_900_imp1.ecsv b/papers/CRACO/ImprovedSurveys/CRAFT_CRACO_900_imp1.ecsv index 19b075d2..69b3b845 100644 --- a/papers/CRACO/ImprovedSurveys/CRAFT_CRACO_900_imp1.ecsv +++ b/papers/CRACO/ImprovedSurveys/CRAFT_CRACO_900_imp1.ecsv @@ -25,5 +25,5 @@ # "TRES": 13.8, "THRESH": 0.309} }'} # schema: astropy-2.0 TNS BW DM DMG FBAR FRES Gb Gl NREP SNR SNRTHRESH THRESH TRES WIDTH XDec XRA Z -DUMMY 288.0 401.4 35.0 906 1.0 "" "" 1 27.9 9.5 0.309 13.8 4.0 "" "" 0.1 +DUMMY 288.0 401.4 35.0 906 1.0 "" "" 1 27.9 9.5 0.2472 13.8 4.0 "" "" 0.1 diff --git a/papers/CRACO/ImprovedSurveys/CRAFT_CRACO_900_imp_all.ecsv b/papers/CRACO/ImprovedSurveys/CRAFT_CRACO_900_imp_all.ecsv index d9d94349..09430019 100644 --- a/papers/CRACO/ImprovedSurveys/CRAFT_CRACO_900_imp_all.ecsv +++ b/papers/CRACO/ImprovedSurveys/CRAFT_CRACO_900_imp_all.ecsv @@ -25,5 +25,5 @@ # "TRES": 1.728, "THRESH": 0.309} }'} # schema: astropy-2.0 TNS BW DM DMG FBAR FRES Gb Gl NREP SNR SNRTHRESH THRESH TRES WIDTH XDec XRA Z -DUMMY 288.0 401.4 35.0 906 0.167 "" "" 1 27.9 9.5 0.309 1.728 4.0 "" "" 0.1 +DUMMY 288.0 401.4 35.0 906 0.167 "" "" 1 27.9 9.5 0.2472 1.728 4.0 "" "" 0.1 diff --git a/papers/CRACO/TestSurveys/CRAFT_CRACO_900_3ms_alldm_zDM.pdf b/papers/CRACO/TestSurveys/CRAFT_CRACO_900_3ms_alldm_zDM.pdf deleted file mode 100644 index edd5a915aecbc0b3be77c6364796bcd93c03192e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 75764 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 diff --git a/papers/CRACO/TestSurveys/CRAFT_CRACO_900_3ms_zDM.pdf b/papers/CRACO/TestSurveys/CRAFT_CRACO_900_3ms_zDM.pdf deleted file mode 100644 index edd5a915aecbc0b3be77c6364796bcd93c03192e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 75764 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 diff --git a/papers/CRACO/plot_900_improvements.py b/papers/CRACO/plot_900_improvements.py index fe1edc6d..665b700b 100644 --- a/papers/CRACO/plot_900_improvements.py +++ b/papers/CRACO/plot_900_improvements.py @@ -95,7 +95,7 @@ def main(): "1: $t_{\\rm res}=1.7$ ms", "2: $\\nu_{\\rm res} = 167$ kHz", "3: Perfect imaging", - "4: $T_{\\rm sys}=25^{\\circ}$ K"] + "4: $T_{\\rm sys}=20^{\\circ}$ K"] linestyles=["-",":","--","-.","-"] nz=400 diff --git a/papers/CRACO/plot_askap_2030.py b/papers/CRACO/plot_askap_2030.py new file mode 100644 index 00000000..3f7a7e31 --- /dev/null +++ b/papers/CRACO/plot_askap_2030.py @@ -0,0 +1,261 @@ +""" +This script creates zdm grids for ASKAP incoherent sum observations. + +It exists partly to calculate relative rates from surveys + +For CHIME 1.28, it's 2.54 +For updated, it's 1.88 + +""" +import os + +from astropy.cosmology import Planck18 +from zdm import cosmology as cos +from zdm import figures +from zdm import parameters +from zdm import survey +from zdm import pcosmic +from zdm import iteration as it +from zdm import loading +from zdm import io +from zdm import optical as opt +from zdm import states +import matplotlib + +defaultsize=14 +ds=4 +font = {'family' : 'Helvetica', + 'weight' : 'normal', + 'size' : defaultsize} +matplotlib.rc('font', **font) + + +import numpy as np +from zdm import survey +from matplotlib import pyplot as plt +import importlib.resources as resources + +def main(): + + # in case you wish to switch to another output directory + + opdir="ImprovedSurveys/" + + # approximate best-fit values from recent analysis + # best-fit from Jordan et al + state = states.load_state("HoffmannHalo25")#,scat="updated",rep=None) + + if not os.path.exists(opdir): + os.mkdir(opdir) + + # Initialise surveys and grids + sdir = resources.files('zdm').joinpath('../papers/CRACO/ImprovedSurveys') + """ + ORIGINAL INVESTIGATION + names=['CRAFT_CRACO_900','CRAFT_CRACO_900_imp1', + 'CRAFT_CRACO_900_imp2.1_div2','CRAFT_CRACO_900_imp2.1_div4','CRAFT_CRACO_900_imp2.1_div8', + 'CRAFT_CRACO_900_imp2.2_div2','CRAFT_CRACO_900_imp2.2_div4','CRAFT_CRACO_900_imp2.2_div8', + 'CRAFT_CRACO_900_imp2.3_div2','CRAFT_CRACO_900_imp2.3_div4','CRAFT_CRACO_900_imp2.3_div8', + 'CRAFT_CRACO_900_imp3' + ] + labels = ["CRACO 900 MHz","1: $T_{\\rm sys}=25^{\\circ}$ K", + "2.1 $t_{\\rm rs}=6.8$\\,ms","2.1 $t_{\\rm rs}=3.4$\\,ms","2.1 $t_{\\rm rs}=1.7$\\,ms", + "2.2 $t_{\\rm rs}=6.8$\\,ms","2.2 $t_{\\rm rs}=3.4$\\,ms","2.2 $t_{\\rm rs}=1.7$\\,ms", + "2.3 $t_{\\rm rs}=6.8$\\,ms","2.3 $t_{\\rm rs}=3.4$\\,ms","2.3 $t_{\\rm rs}=1.7$\\,ms", + "3: $\\nu_{\\rm res}=167\\,kHz" + ] + linestyles=["-","-","-.","-.","-.","--","--","--",":",":",":","-"] + """ + + """ + names=['CRAFT_CRACO_900', + 'CRAFT_CRACO_900_imp2.3_div2','CRAFT_CRACO_900_imp2.3_div4','CRAFT_CRACO_900_imp2.3_div8', + 'CRAFT_CRACO_900_imp2.4_div2','CRAFT_CRACO_900_imp2.4_div4','CRAFT_CRACO_900_imp2.4_div8', + 'CRAFT_CRACO_900_imp2.4_div8_primary', + #'CRAFT_CRACO_900_imp1', + 'CRAFT_CRACO_900_imp_all' + ] + labels = ["CRACO 900 MHz", + "1: $t_{\\rm rs}=6.8$\\,ms","2: $t_{\\rm rs}=3.4$\\,ms","2: $t_{\\rm rs}=1.7$\\,ms", + "2: $t_{\\rm rs}=6.8$\\,ms","3: $t_{\\rm rs}=3.4$\\,ms","3: $t_{\\rm rs}=1.7$\\,ms", + "3: Perfect imaging", + #"4: $T_{\\rm sys}=25^{\\circ}$ K", + "4: $T_{\\rm sys}=25^{\\circ}$ K"] + linestyles=["-",":",":",":","--","--","--","-.","-"] + """ + + + nz=400 + zmax=4 + ndm=500 + dmmax=5000 + + # purely to get normalisation - relative to CRAFT ICS + names = ['CRAFT_ICS_892'] + #names = ['CRAFT_class_I_and_II'] + ss,gs = loading.surveys_and_grids(survey_names=names,repeaters=False, + init_state=state, + zmax=zmax,nz=nz,dmmax=dmmax,ndm=ndm) + + + rate = np.sum(gs[0].get_rates()) + # normalises rate to actual observed rate + actual_rate = ss[0].NORM_FRB/ss[0].TOBS + multiplier = actual_rate/rate + print("Calculated rate multiplier of ",multiplier) + + names=['CRAFT_CRACO_900', + 'CRAFT_CRACO_900_imp1', + 'CRAFT_CRACO_900_imp2.4_div8_primary', + 'CRAFT_CRACO_900_imp_all' + ] + labels = ["CRACO 900 MHz", + "1: $T_{\\rm sys}=20^{\\circ}$ K PAFs", + "2: Perfect CRACO", + "3: Perfect CRACO + $T_{\\rm sys}=25^{\\circ}$ K PAFs"] + linestyles=["-",":","--","-.","-",":","--"] + + + ss,gs = loading.surveys_and_grids(survey_names=names,repeaters=False, + init_state=state,sdir=sdir, + zmax=zmax,nz=nz,dmmax=dmmax,ndm=ndm) + + + + ######### plots total DM and z distribution ####### + # set limits for plots - will be LARGE! + DMmax=4000 + zmax=4. + + plt.figure() + ax1 = plt.gca() + plt.xlabel("redshift $z$") + plt.ylabel("p(z) [a.u.]") + plt.xlim(0.01,3) + plt.ylim(0,1) + #plt.ylim(0,80) + + plt.figure() + ax2 = plt.gca() + plt.xlabel("DM pc cm$^{-3}$") + plt.ylabel("p(DM) [a.u.]") + plt.xlim(0,3000) + plt.ylim(0,1) + #plt.ylim(0,0.0009) + + zvals = gs[0].zvals + dz = zvals[1]-zvals[0] + dmvals = gs[0].dmvals + ddm = dmvals[1]-dmvals[0] + + pzs=[] + pdms=[] + allrates=[] + # chooses the first arbitrarily to extract zvals etc from + for i,g in enumerate(gs): + + s=ss[i] + g=gs[i] + name = names[i] + #figures.plot_grid(gs[i].rates,g.zvals,g.dmvals, + # name=opdir+name+"_zDM.pdf",norm=3,log=True, + # label='$\\log_{10} p({\\rm DM}_{\\rm IGM} + {\\rm DM}_{\\rm host},z)$ [a.u.]', + # project=False,ylabel='${\\rm DM}_{\\rm IGM} + {\\rm DM}_{\\rm host}$', + # zmax=zmax,DMmax=DMmax,Aconts=[0.01,0.1,0.5]) + + rates = gs[i].get_rates() #gs[i].rates * 10**g.state.FRBdemo.lC + rate = np.sum(rates)*multiplier + allrates.append(rate) + pz = np.sum(rates,axis=1) + pz /= dz + + pdm = np.sum(rates,axis=0) + pdm /= ddm + + pzs.append(pz) + pdms.append(pdm) + + inorm=3 + for i,g in enumerate(gs): + pz = pzs[i]/np.max(pzs[inorm]) + pdm = pdms[i]/np.max(pdms[inorm]) + + print("Rate for ",names[i]," is ",allrates[i], "(relative rate: ",allrates[i]/allrates[0],") per day") + + plt.sca(ax1) + plt.plot(zvals,pz,label=labels[i],linestyle=linestyles[i]) + + plt.sca(ax2) + plt.plot(dmvals,pdm,label=labels[i],linestyle=linestyles[i]) + + + + plt.sca(ax1) + plt.savefig("ATNF_2030/nolegend_improved_zs.png") + plt.legend(fontsize=12,loc="upper right") + plt.tight_layout() + plt.savefig("ATNF_2030/improved_zs.png") + plt.close() + + plt.sca(ax2) + plt.tight_layout() + plt.savefig("ATNF_2030/nolegend_improved_dms.png") + plt.legend(fontsize=12,loc="upper right") + plt.tight_layout() + plt.savefig("ATNF_2030/improved_dms.png") + plt.close() + +def plot_efficiencies(gs,ss): + """ + Does some efficiency plots + """ + ###### plots efficiencies ###### + plt.figure() + for i,s in enumerate(ss): + + for j in np.arange(s.NWbins): + if j==0: + plt.plot(s.dmvals,s.efficiencies[j,:],linestyle=linestyles[i],label=labels[i]) + else: + plt.plot(s.dmvals,s.efficiencies[j,:],linestyle=linestyles[i],color=plt.gca().lines[-1].get_color()) + plt.xlabel("DM") + plt.ylabel("Efficiency") + plt.legend() + plt.tight_layout() + plt.savefig("Plots/efficiency.png") + plt.close() + + + ##### Plots an example of the threshold ###### + plt.figure() + for i,g in enumerate(gs): + print("Survey weights are ",ss[i].wlist,ss[i].wplist) + for j in np.arange(g.nthresh): + if j==0: + plt.plot(g.dmvals,g.thresholds[j,10,:],linestyle=linestyles[i],label=labels[i],linewidth=0.2) + else: + plt.plot(g.dmvals,g.thresholds[j,10,:],linestyle=linestyles[i],color=plt.gca().lines[-1].get_color(),linewidth=j) + plt.xlabel("DM") + plt.ylabel("Threshold (erg)") + plt.legend() + plt.tight_layout() + plt.savefig("Plots/g_thresholds.png") + plt.close() + + +def check_FE(state): + """ + Checks FRB rate compared to Fly's Eye rate, which is the most reliable and consistent + """ + ###### Checks normalisation ###### + ss,gs = loading.surveys_and_grids( + survey_names=["CRAFT_class_I_and_II"],repeaters=False,init_state=state) # should be equal to actual number of FRBs, but for this purpose it doesn't matter + + rate = np.sum(gs[0].rates) * 10**gs[0].state.FRBdemo.lC * ss[0].TOBS + + print("Expected number for Fly's Eys is ",rate," per day") + print("c.f. actual number: ",ss[0].NORM_FRB) + + + +main() diff --git a/papers/Casatta/plot_casatta.py b/papers/Casatta/plot_casatta.py index 9c0198e4..5fdb2b15 100644 --- a/papers/Casatta/plot_casatta.py +++ b/papers/Casatta/plot_casatta.py @@ -47,7 +47,7 @@ def main(): dz = zvals[1]-zvals[0] plt.xlabel("z") plt.ylabel("p(z) [FRBs / day / z]") - plt.ylim(1e-3,1e5) + plt.ylim(1e-1,1e7) for isim in np.arange(nsims): plt.plot(zvals,pzs[isim,:]/dz,label=df["Array_name"][isim]) plt.legend(fontsize=4) @@ -58,7 +58,7 @@ def main(): plt.figure() plt.yscale("log") - plt.ylim(1e-6,1e2) + plt.ylim(1e-4,1e4) # multiplies by DM width ddm = dmvals[1]-dmvals[0] @@ -66,6 +66,7 @@ def main(): plt.ylabel("p(DM) [FRBs /day /pc cm$^{-3}$]") for isim in np.arange(nsims): plt.plot(dmvals,pdms[isim,:]/ddm,label=df["Array_name"][isim]) + print("Daily rate for sim ",isim,": ",df["Array_name"][isim], " is ",dailys[isim]) plt.legend(fontsize=4) plt.tight_layout() plt.savefig("all_pdm.png") @@ -80,9 +81,10 @@ def main(): plt.ylabel("Daily rate") plt.xscale("log") plt.yscale("log") - plt.scatter(dailys,FOM) - \ - plt.plot([1e-5,1e4],[0.05,5e7],color="black",label="1-1 line",linestyle="--") + plt.scatter(FOM,dailys) + plt.ylim(1e-4,1e8) + + plt.plot([1e-2,1e8],[1e-3,1e7],color="black",label="1-1 line",linestyle="--") plt.legend() plt.tight_layout() plt.savefig("FOM.png") diff --git a/papers/Casatta/sim_casatta.py b/papers/Casatta/sim_casatta.py index 28fbeb06..5272e941 100644 --- a/papers/Casatta/sim_casatta.py +++ b/papers/Casatta/sim_casatta.py @@ -39,7 +39,8 @@ def main(): # we can keep this constant - it smears DM due to host DM mask = pcosmic.get_dm_mask(dmvals, (state.host.lmean, state.host.lsigma), zvals, plot=False) - sim = True + # gets constant of total FRB rate to normalise to + renorm = get_constant(state,zDMgrid,zvals,dmvals,mask) threshs = np.zeros([nsims]) dailys = np.zeros([nsims]) @@ -47,13 +48,15 @@ def main(): pdms = np.zeros([nsims,dmvals.size]) for isim in np.arange(nsims): - daily,pz,pdm,thresh = sim_casatta(df.iloc[isim],state,zDMgrid, zvals, dmvals,mask) + daily,pz,pdm,thresh = sim_casatta(df.iloc[isim],state,zDMgrid,zvals,dmvals,mask) dailys[isim]=daily pzs[isim,:]=pz pdms[isim,:]=pdm threshs[isim] = thresh - print("Done simulation ",isim, "daily rate ",daily) + print("Done simulation ",isim, " of ", nsims,", daily rate ",daily*renorm) + # modifies rates according to expectations + dailys *= renorm np.save("threshs.npy",threshs) np.save("dailys.npy",dailys) np.save("pzs.npy",pzs) @@ -117,20 +120,6 @@ def sim_casatta(df,state,zDMgrid, zvals, dmvals, mask): survey_dict = {"THRESH": THRESH, "TOBS": 1, "FBAR": float(fMHz), "BW": float(BW), "DIAM": DIAM, "FRES": float(fres), "TRES": float(tres)} - #uncomment this to calculate the rates for Fly's Eye - if False: - s = survey.load_survey("CRAFT_class_I_and_II", state, dmvals, zvals=zvals) - - g = zdm_grid.Grid(s, copy.deepcopy(state), zDMgrid, zvals, dmvals, mask, wdist=True) - - predicted = np.sum(g.rates)* s.TOBS * 10**state.FRBdemo.lC - expected = s.NORM_FRB - - print("expected ",expected,predicted) - - exit() - - survey_name = "casatta_base" s = survey.load_survey(survey_name, state, dmvals, zvals=zvals, survey_dict=survey_dict, sdir=sdir) @@ -143,7 +132,32 @@ def sim_casatta(df,state,zDMgrid, zvals, dmvals, mask): pdm = np.sum(g.rates,axis=0)* 10**state.FRBdemo.lC return daily,pz,pdm,THRESH + +def get_constant(state,zDMgrid, zvals, dmvals, mask): + """ + gets a normalising constant for this state + + Args: + df: dataframe containing info for this version of casatta + state: zdm state object + zDMgrid: underlying zDM grid giving p(DMcosmic|z) + zvals: redshift values of grid + dmvals: DM values of grid + mask: DM smearing mask for grid based on DMhost + """ + # I am here choosing to renomalise by the CRAFT ICS 892 MHz rates + #norm_survey = "CRAFT_class_I_and_II" + norm_survey = "CRAFT_ICS_892" + s = survey.load_survey(norm_survey, state, dmvals, zvals=zvals) + g = zdm_grid.Grid(s, copy.deepcopy(state), zDMgrid, zvals, dmvals, mask, wdist=True) + + predicted = np.sum(g.rates) * s.TOBS * 10**state.FRBdemo.lC + observed = s.NORM_FRB + renorm = observed/predicted + print("Calculated renomalisation constant as ",renorm) + return renorm + main() diff --git a/papers/pathpriors/compare_posteriors.py b/papers/pathpriors/compare_posteriors.py index adc6cb14..d401cfc2 100644 --- a/papers/pathpriors/compare_posteriors.py +++ b/papers/pathpriors/compare_posteriors.py @@ -47,6 +47,7 @@ def main(): ######### List of all ICS FRBs for which we can run PATH ####### # hard-coded list of FRBs with PATH data in ice paper frblist=opt.frblist + frblist.remove('FRB20230731A') # too reddened # Initlisation of zDM grid # Eventually, this should be part of the loop, i.e. host IDs should @@ -71,9 +72,10 @@ def main(): # calculates the original PATH result wrappers = None NFRB2,AppMags2,AppMagPriors2,ObsMags2,ObsPrior2,ObsPosteriors2,PUprior2,PUobs2,sumPUprior2,sumPUobs2,frbs,dms = \ - on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False,usemodel=False,P_U=0.) + on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False,usemodel=False,P_U=0.1) fObsPosteriors2 = on.flatten(ObsPosteriors2) + with open("posteriors/orig.txt",'w') as f: for i,frb in enumerate(frbs): f.write(str(i)+" "+frb+" "+str(dms[i])[0:5]+" "+str(PUprior2[i])[0:4]+"\n") @@ -88,34 +90,34 @@ def main(): model = opt.marnoch_model() wrappers = on.make_wrappers(model,gs) - NFRB,AppMags,AppMagPriors,ObsMags,ObsPrior,ObsPosteriors,PUprior,PUobs,sumPUprior,sumPUobs,frbs,dms = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) + NFRB1,AppMags1,AppMagPriors1,ObsMags1,ObsPrior1,ObsPosteriors1,PUprior1,PUobs1,sumPUprior1,sumPUobs1,frbs,dms = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) - fObsPosteriors = on.flatten(ObsPosteriors) - plt.scatter(fObsPosteriors2,fObsPosteriors,label="Marnoch",marker='s') + fObsPosteriors1 = on.flatten(ObsPosteriors1) + plt.scatter(fObsPosteriors2,fObsPosteriors1,label="Marnoch",marker='s') with open("posteriors/marnoch.txt",'w') as f: for i,frb in enumerate(frbs): - f.write(str(i)+" "+frb+" "+str(dms[i])[0:5]+" "+str(PUprior[i])[0:4]+"\n") - for j,om in enumerate(ObsMags[i]): - f.write(str(om)[0:5]+" "+ "%e" % ObsPrior[i][j]+" "+"%e" % ObsPosteriors[i][j]+"\n") + f.write(str(i)+" "+frb+" "+str(dms[i])[0:5]+" "+str(PUprior1[i])[0:4]+"\n") + for j,om in enumerate(ObsMags1[i]): + f.write(str(om)[0:5]+" "+ "%e" % ObsPrior1[i][j]+" "+"%e" % ObsPosteriors1[i][j]+"\n") f.write("\n") ####### Model 2: Loudas ######## model = opt.loudas_model() - model.init_args([1.68]) # best-fit arguments + model.init_args([1.49]) # best-fit arguments wrappers = on.make_wrappers(model,gs) - NFRB,AppMags,AppMagPriors,ObsMags,ObsPrior,ObsPosteriors,PUprior,PUobs,sumPUprior,sumPUobs,frbs,dms = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) + NFRB3,AppMags3,AppMagPriors3,ObsMags3,ObsPrior3,ObsPosteriors3,PUprior3,PUobs3,sumPUprior3,sumPUobs3,frbs,dms = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) - fObsPosteriors = on.flatten(ObsPosteriors) - plt.scatter(fObsPosteriors2,fObsPosteriors,label="Loudas",marker='x') + fObsPosteriors3 = on.flatten(ObsPosteriors3) + plt.scatter(fObsPosteriors2,fObsPosteriors3,label="Loudas",marker='x') with open("posteriors/loudas.txt",'w') as f: for i,frb in enumerate(frbs): - f.write(str(i)+" "+frb+" "+str(dms[i])[0:5]+" "+str(PUprior[i])[0:4]+"\n") - for j,om in enumerate(ObsMags[i]): - f.write(str(om)[0:5]+" "+ "%e" % ObsPrior[i][j]+" "+"%e" % ObsPosteriors[i][j]+"\n") + f.write(str(i)+" "+frb+" "+str(dms[i])[0:5]+" "+str(PUprior3[i])[0:4]+"\n") + for j,om in enumerate(ObsMags3[i]): + f.write(str(om)[0:5]+" "+ "%e" % ObsPrior3[i][j]+" "+"%e" % ObsPosteriors3[i][j]+"\n") f.write("\n") @@ -130,32 +132,63 @@ def main(): model = opt.simple_host_model(opstate) # retrieve default initial arguments in vector form - x = [-2.29467289 ,0. , 0. , 0. ,0.1109831,0.72688895, 1. , 0. , 0. , 0. , 0.] + x = [-2.28795519, 0., 0. , 0. , 0.11907231,0.84640048,0.99813815 , 0., 0. , 0. , 0. ] # initialises best-fit arguments model.init_args(x) ############# Generate a KS-like plot showing the best fits #################### wrappers = on.make_wrappers(model,gs) - NFRB,AppMags,AppMagPriors,ObsMags,ObsPrior,ObsPosteriors,PUprior,PUobs,sumPUprior,sumPUobs,frbs,dms = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) + NFRB4,AppMags4,AppMagPriors4,ObsMags4,ObsPrior4,ObsPosteriors4,PUprior4,PUobs4,sumPUprior4,sumPUobs4,frbs,dms = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) - fObsPosteriors = on.flatten(ObsPosteriors) - plt.scatter(fObsPosteriors2,fObsPosteriors,label="Naive",marker='o',s=20) + fObsPosteriors4 = on.flatten(ObsPosteriors4) + plt.scatter(fObsPosteriors2,fObsPosteriors4,label="Naive",marker='o',s=20) plt.legend(loc="lower right") plt.xlim(0,1) plt.ylim(0,1) plt.plot([0,1],[0,1],color="black",linestyle=":") plt.tight_layout() - plt.savefig("pox_comparison.png") + plt.savefig("posteriors/pox_comparison.png") plt.close() with open("posteriors/naive.txt",'w') as f: for i,frb in enumerate(frbs): - f.write(str(i)+" "+frb+" "+str(dms[i])[0:5]+" "+str(PUprior[i])[0:4]+"\n") - for j,om in enumerate(ObsMags[i]): - f.write(str(om)[0:5]+" "+ "%e" % ObsPrior[i][j]+" "+"%e" % ObsPosteriors[i][j]+"\n") + f.write(str(i)+" "+frb+" "+str(dms[i])[0:5]+" "+str(PUprior4[i])[0:4]+"\n") + for j,om in enumerate(ObsMags4[i]): + f.write(str(om)[0:5]+" "+ "%e" % ObsPrior4[i][j]+" "+"%e" % ObsPosteriors4[i][j]+"\n") f.write("\n") - + all_candidates = on.get_cand_properties(frblist) + + # now iterates through galaxies and writes relevant info + for i in np.arange(NFRB1): + string1="\multicolumn{5}{c|}{"+frbs[i]+"} & " + string1 += f"{PUprior2[i]:.3f} & {PUobs2[i]:.3f} & " + string1 += f"{PUprior1[i]:.3f} & {PUobs1[i]:.3f} & " + string1 += f"{PUprior3[i]:.3f} & {PUobs3[i]:.3f} & " + string1 += f"{PUprior4[i]:.3f} & {PUobs4[i]:.3f} \\\\ " + print("\\hline") + print(string1) + print("\\hline") + + + + for j,mag in enumerate(ObsMags4[i]): + # check if we print this one at all + if ObsPosteriors1[i][j] < 1e-4 and ObsPosteriors2[i][j] < 1e-4 \ + and ObsPosteriors3[i][j] < 1e-4 and ObsPosteriors4[i][j] < 1e-4: + + continue + + string2 = f"{all_candidates[i]['ra'][j]:.4f} & ${all_candidates[i]['dec'][j]:.4f}$ &" + string2 += f" {all_candidates[i]['separation'][j]:.2f} &" + string2 += f" {all_candidates[i]['ang_size'][j]:.2f} & {all_candidates[i]['mag'][j]:.2f} &" + + + string2 += f"{ObsPrior2[i][j]:.3f} & {ObsPosteriors2[i][j]:.3f} & " + string2 += f"{ObsPrior1[i][j]:.3f} & {ObsPosteriors1[i][j]:.3f} & " + string2 += f"{ObsPrior3[i][j]:.3f} & {ObsPosteriors3[i][j]:.3f} & " + string2 += f"{ObsPrior4[i][j]:.3f} & {ObsPosteriors4[i][j]:.3f} \\\\ " + print(string2) main() diff --git a/papers/pathpriors/fit_loudas_model.py b/papers/pathpriors/fit_loudas_model.py index d5cb68de..50920da0 100644 --- a/papers/pathpriors/fit_loudas_model.py +++ b/papers/pathpriors/fit_loudas_model.py @@ -42,6 +42,7 @@ def main(): ######### List of all ICS FRBs for which we can run PATH ####### # hard-coded list of FRBs with PATH data in ice paper frblist=opt.frblist + frblist.remove('FRB20230731A') # too reddened # Initlisation of zDM grid # Eventually, this should be part of the loop, i.e. host IDs should @@ -78,7 +79,7 @@ def main(): args=[frblist,ss,gs,model,POxcut,istat] # turn minimise on to re-perform the minimusation. But it's already been done - minimise=False + minimise=True if minimise: result = minimize(on.function,x0 = x0,args=args,bounds = bounds) print("Best fit result is f_sfr = ",result.x) @@ -87,15 +88,15 @@ def main(): np.save(opdir+"/best_fit_params.npy",x) else: # replace later - x=[1.68] + x=[1.49] print("using previous result of f_sfr = ",x) # initialises arguments model.init_args(x) - outfile = opdir+"best_fit_apparent_magnitudes.png" + outfile = opdir+"loudas_best_fit_apparent_magnitudes.png" wrappers = on.make_wrappers(model,gs) - NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUprior,PUobs,sumPUprior,sumPUobs = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) + NFRB,AppMags,AppMagPriors,ObsMags,ObsPriors,ObsPosteriors,PUprior,PUobs,sumPUprior,sumPUobs,frbs,dms = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) llstat = on.calculate_likelihood_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUobs,PUprior,plotfile=outfile) ksstat = on.calculate_ks_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,sumPUobs,sumPUprior,plotfile=outfile) @@ -114,11 +115,11 @@ def main(): sumPUpriorlist = [] sumPUobslist = [] - for f_sfr in [1.68,0,1]: + for f_sfr in [1.49,0,1]: x=[f_sfr] model.init_args(x) wrappers = on.make_wrappers(model,gs) - NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUprior,PUobs,sumPUprior,sumPUobs = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) + NFRB,AppMags,AppMagPriors,ObsMags,ObsPriors,ObsPosteriors,PUprior,PUobs,sumPUprior,sumPUobs,frbs,dms = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) NFRBlist.append(NFRB) AppMagslist.append(AppMags) @@ -131,7 +132,8 @@ def main(): sumPUobslist.append(sumPUobs) NMODELS = 3 - plotlabels=["$f_{\\rm sfr} = 1.68$","$f_{\\rm sfr} = 0$", "$f_{\\rm sfr} = 1$"] + + plotlabels=["$f_{\\rm sfr} = 1.49$","$f_{\\rm sfr} = 0$", "$f_{\\rm sfr} = 1$"] plotfile = opdir+"loudas_f0_1_best_comparison.png" on.make_cumulative_plots(NMODELS,NFRBlist,AppMagslist,AppMagPriorslist,ObsMagslist,ObsPosteriorslist, PUobslist,PUpriorlist,plotfile,plotlabels,POxcut=None,onlyobs=0,abc="(b)") @@ -144,8 +146,8 @@ def main(): model.init_args([sfr]) wrappers = on.make_wrappers(model,gs) - NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUprior,\ - PUobs,sumPUprior,sumPUobs = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) + NFRB,AppMags,AppMagPriors,ObsMags,ObsPriors,ObsPosteriors,PUprior,\ + PUobs,sumPUprior,sumPUobs,frbs,dms = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) stat = on.calculate_likelihood_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUobs, PUprior,plotfile=outfile,POxcut=POxcut) stats[istat] = stat @@ -155,7 +157,7 @@ def main(): plt.xlabel("$f_{\\rm sfr}$") plt.ylabel("$\\log_{10} \\mathcal{L}(f_{\\rm sfr})$") plt.xlim(0,3) - plt.ylim(46,52) + plt.ylim(48,53) plt.tight_layout() plt.savefig(outfile) plt.close() diff --git a/papers/pathpriors/fit_simple_model.py b/papers/pathpriors/fit_simple_model.py index 3106eaeb..4abf7c02 100644 --- a/papers/pathpriors/fit_simple_model.py +++ b/papers/pathpriors/fit_simple_model.py @@ -46,6 +46,8 @@ def main(): ######### List of all ICS FRBs for which we can run PATH ####### # hard-coded list of FRBs with PATH data in ice paper frblist=opt.frblist + frblist.remove('FRB20230731A') # too reddened + # Initlisation of zDM grid # Eventually, this should be part of the loop, i.e. host IDs should @@ -53,9 +55,11 @@ def main(): # with very limited redshift estimates. That might require posterior # estimates of redshift given the observed galaxies. Maybe. state = states.load_state("HoffmannHalo25",scat=None,rep=None) - #state = parameters.State() + + # initialise cosmology cos.set_cosmology(state) cos.init_dist_measures() + names=['CRAFT_ICS_892','CRAFT_ICS_1300','CRAFT_ICS_1632'] ss,gs = loading.surveys_and_grids(survey_names=names,init_state=state) @@ -120,17 +124,18 @@ def main(): else: # hard-coded best fit parameters from running optimisation if not dok: - x = [0.,0.,0.,0.18691816,0.99999953,0.44064664,0.,0.,0.,0.] + print("Need to re-run this!") + exit() else: - x = [-2.29467289 ,0. , 0. , 0. ,0.1109831,0.72688895, 1. , 0. , 0. , 0. , 0.] - + x = [-2.28795519,0.,0.,0.,0.11907231,0.84640048,0.99813815,0.,0.,0.,0.] # initialises best-fit arguments model.init_args(x) ############# Generate a KS-like plot showing the best fits #################### - outfile = opdir+"best_fit_apparent_magnitudes.png" + outfile = opdir+"simple_best_fit_apparent_magnitudes.png" wrappers = on.make_wrappers(model,gs) - NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUprior,PUobs,sumPUprior,sumPUobs = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) + NFRB,AppMags,AppMagPriors,ObsMags,ObsPriors,ObsPosteriors,PUprior,PUobs,sumPUprior,sumPUobs,frbs,dms = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) + llstat = on.calculate_likelihood_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUobs,PUprior,plotfile=outfile) ksstat = on.calculate_ks_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,sumPUobs, sumPUprior,plotfile=outfile,abc="(c)",tag="naive: ",) @@ -147,7 +152,7 @@ def main(): x[0] = kcorr model.init_args(x) wrappers = on.make_wrappers(model,gs) - NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUprior,PUobs,sumPUprior,sumPUobs = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) + NFRB,AppMags,AppMagPriors,ObsMags,ObsPriors,ObsPosteriors,PUprior,PUobs,sumPUprior,sumPUobs,frbs,dms = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) stat = on.calculate_likelihood_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUobs,PUprior) stats[i] = stat @@ -157,14 +162,13 @@ def main(): plt.xlabel('$k$') plt.ylabel('$\\log_{10} \\mathcal{L} (k)$') plt.tight_layout() - plt.savefig('pkvalue.png') + plt.savefig(opdir+'/pkvalue.png') plt.close() # calculates the original PATH result outfile = opdir+"original_fit_apparent_magnitudes.png" - NFRB2,AppMags2,AppMagPriors2,ObsMags2,ObsPosteriors2,PUprior2,PUobs2,sumPUprior2,sumPUobs2 = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False,usemodel=False) - stat = on.calculate_ks_statistic(NFRB,AppMags2,AppMagPriors2,ObsMags2,ObsPosteriors2,sumPUobs2,sumPUprior2,plotfile=outfile) + NFRB2,AppMags2,AppMagPriors2,ObsMags2,ObsPriors2,ObsPosteriors2,PUprior2,PUobs2,sumPUprior2,sumPUobs2,frbs,dms = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False,usemodel=False) # flattens lists of lists ObsPosteriors = [x for xs in ObsPosteriors for x in xs] diff --git a/papers/pathpriors/pU_g_mr/test_pogmr.py b/papers/pathpriors/pU_g_mr/fit_pogmr.py similarity index 100% rename from papers/pathpriors/pU_g_mr/test_pogmr.py rename to papers/pathpriors/pU_g_mr/fit_pogmr.py diff --git a/papers/pathpriors/plot_marnoch_model.py b/papers/pathpriors/plot_marnoch_model.py index c5a887e6..54f98053 100644 --- a/papers/pathpriors/plot_marnoch_model.py +++ b/papers/pathpriors/plot_marnoch_model.py @@ -42,6 +42,7 @@ def main(): ######### List of all ICS FRBs for which we can run PATH ####### # hard-coded list of FRBs with PATH data in ice paper frblist=opt.frblist + frblist.remove('FRB20230731A') # too reddened # Initlisation of zDM grid # Eventually, this should be part of the loop, i.e. host IDs should @@ -71,9 +72,9 @@ def main(): model = opt.marnoch_model() - outfile = opdir+"best_fit_apparent_magnitudes.png" + outfile = opdir+"marnoch_best_fit_apparent_magnitudes.png" wrappers = on.make_wrappers(model,gs) - NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUprior,PUobs,sumPUprior,sumPUobs = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) + NFRB,AppMags,AppMagPriors,ObsMags,ObsPriors,ObsPosteriors,PUprior,PUobs,sumPUprior,sumPUobs,frbs,dms = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) llstat = on.calculate_likelihood_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUobs,PUprior,plotfile=outfile) ksstat = on.calculate_ks_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,sumPUobs, diff --git a/zdm/optical.py b/zdm/optical.py index 296664fc..36fcc3e7 100644 --- a/zdm/optical.py +++ b/zdm/optical.py @@ -1031,6 +1031,47 @@ def estimate_unseen_prior(self): return self.PU + def path_base_prior(self,mags): + """ + Calculates base magnitude prior. Does NOT include + galaxy density factor + """ + ngals = len(mags) + Ois = [] + for i,mag in enumerate(mags): + + #print(mag) + # calculate the bins in apparent magnitude prior + kmag2 = (mag - self.Appmin)/self.dAppmag + imag1 = int(np.floor(kmag2)) + imag2 = imag1 + 1 + kmag2 -= imag1 #residual; float + kmag1 = 1.-kmag2 + + # careful with interpolation - priors are for magnitude bins + # with bin edges give by Appmin + N dAppmag. + # We probably want to smooth this eventually due to minor + # numerical tweaks + + #kmag2 -= imag1 + #kmag1 = 1.-kmag2 + #imag2 = imag1+1 + #prior = kmag1*self.priors[imag1] + kmag2*self.priors[imag2] + + # simple linear interpolation + Oi = self.priors[imag1] * kmag1 + self.priors[imag2] * kmag2 + + # correct normalisation - otherwise, priors are defined to sum + # such that \sum priors = 1; here, we need \int priors dm = 1 + Oi /= self.dAppmag + + Ois.append(Oi) + + Ois = np.array(Ois) + return Ois + + + def path_raw_prior_Oi(self,mags,ang_sizes,Sigma_ms): """ Function to pass to astropath module @@ -1088,6 +1129,7 @@ def path_raw_prior_Oi(self,mags,ang_sizes,Sigma_ms): Oi /= self.dAppmag Oi /= Sigma_ms[i] # normalise by host counts + Ois.append(Oi) Ois = np.array(Ois) @@ -1421,7 +1463,7 @@ def matchFRB(TNSname,survey): frblist=['FRB20180924B','FRB20181112A','FRB20190102C','FRB20190608B', 'FRB20190611B','FRB20190711A','FRB20190714A','FRB20191001A', 'FRB20191228A','FRB20200430A','FRB20200906A','FRB20210117A', - 'FRB20210320C','FRB20210807D','FRB20211127I','FRB20211203C', + 'FRB20210320C','FRB20210807D','FRB20210912A','FRB20211127I','FRB20211203C', 'FRB20211212A','FRB20220105A','FRB20220501C', 'FRB20220610A','FRB20220725A','FRB20220918A', 'FRB20221106A','FRB20230526A','FRB20230708A', diff --git a/zdm/optical_numerics.py b/zdm/optical_numerics.py index 33ed00f9..3de9a8f7 100644 --- a/zdm/optical_numerics.py +++ b/zdm/optical_numerics.py @@ -12,6 +12,11 @@ from zdm import optical as op +from frb.frb import FRB +from astropath.priors import load_std_priors +from astropath.path import PATH +from frb.associate import frbassociate + def function(x,args): """ This is a function for input into the scipi.optimize.minimise routine. @@ -42,7 +47,7 @@ def function(x,args): model.init_args(x) wrappers = make_wrappers(model,gs) - NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUprior,PUobs,sumPUprior,sumPUobs = calc_path_priors(frblist,ss,gs,wrappers,verbose=False) + NFRB,AppMags,AppMagPriors,ObsMags,ObsPriors,ObsPosteriors,PUprior,PUobs,sumPUprior,sumPUobs,frbs,dms = calc_path_priors(frblist,ss,gs,wrappers,verbose=False) # we re-normalise the sum of PUs by NFRB @@ -88,8 +93,8 @@ def make_cdf(xs,ys,ws,norm = True): if norm: cdf /= cdf[-1] return cdf - + def calc_path_priors(frblist,ss,gs,wrappers,verbose=True,usemodel=True,P_U=0.1): """ Inner loop. Gets passed model parameters, but assumes everything is @@ -193,6 +198,11 @@ def calc_path_priors(frblist,ss,gs,wrappers,verbose=True,usemodel=True,P_U=0.1): P_O,P_Ox,P_Ux,ObsMags,ptbl = run_path(frb,usemodel=usemodel,P_U = P_U) + + # replaces PO value with raw PO value, i.e. excluding the driver sigma + if usemodel: + P_O = wrapper.path_base_prior(ObsMags) + # kept here for debugging if False: print("P_U is ",P_U) @@ -500,6 +510,34 @@ def make_cumulative_plots(NMODELS,NFRB,AppMags,AppMagPriors,ObsMags,ObsPosterior return None + + +def get_cand_properties(frblist): + """ + Returns properties of galaxy candidates for FRBs + + Args: + frblist: list of strings giving FRB names + + Returns: + all_candidates: list of pandas dataframes containing candidate info + """ + + all_candidates=[] + for i,name in enumerate(frblist): + + ######### Loads FRB, and modifes properties ######### + my_frb = FRB.by_name(name) + #this_path = frbassociate.FRBAssociate(my_frb, max_radius=10.) + + # reads in galaxy info + ppath = os.path.join(resources.files('frb'), 'data', 'Galaxies', 'PATH') + pfile = os.path.join(ppath, f'{my_frb.frb_name}_PATH.csv') + ptbl = pandas.read_csv(pfile) + candidates = ptbl[['ang_size', 'mag', 'ra', 'dec', 'separation']] + all_candidates.append(candidates) + return all_candidates + def run_path(name,P_U=0.1,usemodel = False, sort=False): """ evaluates PATH on an FRB @@ -510,27 +548,24 @@ def run_path(name,P_U=0.1,usemodel = False, sort=False): sort [bool]: if True, sort candidates by posterior """ - from frb.frb import FRB - from astropath.priors import load_std_priors - from astropath.path import PATH ######### Loads FRB, and modifes properties ######### my_frb = FRB.by_name(name) + this_path = frbassociate.FRBAssociate(my_frb, max_radius=10.) + - # do we even still need this? I guess not, but will keep it here just in case - my_frb.set_ee(my_frb.sig_a,my_frb.sig_b,my_frb.eellipse['theta'], - my_frb.eellipse['cl'],True) + # do NOT do the below method! + # + + # do NOT do the below!! + #my_frb.set_ee(my_frb.sig_a,my_frb.sig_b,my_frb.eellipse['theta'], + # my_frb.eellipse['cl'],True) # reads in galaxy info ppath = os.path.join(resources.files('frb'), 'data', 'Galaxies', 'PATH') pfile = os.path.join(ppath, f'{my_frb.frb_name}_PATH.csv') ptbl = pandas.read_csv(pfile) - - if name=="FRB20181112A": - print(ptbl) - exit() - ngal = len(ptbl) ptbl["frb"] = np.full([ngal],name) @@ -548,7 +583,8 @@ def run_path(name,P_U=0.1,usemodel = False, sort=False): candidates = ptbl[['ang_size', 'mag', 'ra', 'dec', 'separation']] - this_path = PATH() + + #this_path = PATH() this_path.init_candidates(candidates.ra.values, candidates.dec.values, candidates.ang_size.values, @@ -569,7 +605,7 @@ def run_path(name,P_U=0.1,usemodel = False, sort=False): this_path.init_cand_prior('user', P_U=prior['U']) else: this_path.init_cand_prior('inverse', P_U=prior['U']) - + # this is for the offset this_path.init_theta_prior(prior['theta']['method'], prior['theta']['max'], diff --git a/zdm/states.py b/zdm/states.py index d80dba3c..02a8432d 100644 --- a/zdm/states.py +++ b/zdm/states.py @@ -242,7 +242,7 @@ def set_fit_params(vparams,case): vparams['FRBdemo']['alpha_method'] = 1 vparams['FRBdemo']['source_evolution'] = 0 vparams['FRBdemo']['sfr_n'] = 2.88 - vparams['FRBdemo']['lC'] = -8.82 + vparams['FRBdemo']['lC'] = -6.24 vparams['host']['lmean'] = 2.13 vparams['host']['lsigma'] = 0.46 diff --git a/zdm/survey.py b/zdm/survey.py index 78b4e401..11e4e67d 100644 --- a/zdm/survey.py +++ b/zdm/survey.py @@ -928,10 +928,20 @@ def process_survey_file(self,filename:str, # Meta -- for convenience for now; best to migrate away from this + default_telescope = survey_data.Telescope() + for key in self.survey_data.params: DC = self.survey_data.params[key] - self.meta[key] = getattr(self.survey_data[DC],key) - + if DC == "telescope": + value = getattr(self.survey_data[DC],key) + if value == getattr(default_telescope,key): + # using default value - check if the FRBs have this + if key in frb_tbl.columns: + value = np.mean(frb_tbl[key]) + self.meta[key] = value + else: + self.meta[key] = getattr(self.survey_data[DC],key) + # Get default values from default frb data default_frb = survey_data.FRB() @@ -947,8 +957,8 @@ def process_survey_file(self,filename:str, # now checks for missing data, fills with the default value if field.name in frb_tbl.columns: - - # iterate over fields, checking if they are populated + # iterate over fields, checking if they are populated. + # only replaces values that are [] for i,val in enumerate(frb_tbl[field.name]): if isinstance(val,np.ma.core.MaskedArray): frb_tbl[field.name][i] = default_value From 15df6423ccc0dbbf491032bdb9ba8ae7ea2e3b77 Mon Sep 17 00:00:00 2001 From: Clancy James Date: Thu, 12 Feb 2026 14:21:05 +0800 Subject: [PATCH 18/35] Made changes to include MeerTRAP in H0 estimation, and fixed issue with MeerTRAP coherent ecsv --- .../lsst/Photometric/{ => CRACO}/Smeared.ecsv | 0 .../{ => CRACO}/Smeared_and_zFrac.ecsv | 0 .../{ => CRACO}/Spectroscopic.ecsv | 0 .../lsst/Photometric/{ => CRACO}/zFrac.ecsv | 0 papers/lsst/Photometric/README.txt | 25 ++++ papers/lsst/Photometric/create_fake_survey.py | 130 +++++++++++++----- papers/lsst/Photometric/plot_2dgrids.py | 15 +- papers/lsst/Photometric/plot_H0_slice.py | 32 +++-- papers/lsst/Photometric/run_H0_slice.py | 8 +- papers/lsst/Photometric/run_slice.py | 5 + zdm/data/Surveys/MeerTRAPcoherent.ecsv | 2 +- zdm/grid.py | 8 +- zdm/survey.py | 2 + 13 files changed, 170 insertions(+), 57 deletions(-) rename papers/lsst/Photometric/{ => CRACO}/Smeared.ecsv (100%) rename papers/lsst/Photometric/{ => CRACO}/Smeared_and_zFrac.ecsv (100%) rename papers/lsst/Photometric/{ => CRACO}/Spectroscopic.ecsv (100%) rename papers/lsst/Photometric/{ => CRACO}/zFrac.ecsv (100%) create mode 100644 papers/lsst/Photometric/README.txt diff --git a/papers/lsst/Photometric/Smeared.ecsv b/papers/lsst/Photometric/CRACO/Smeared.ecsv similarity index 100% rename from papers/lsst/Photometric/Smeared.ecsv rename to papers/lsst/Photometric/CRACO/Smeared.ecsv diff --git a/papers/lsst/Photometric/Smeared_and_zFrac.ecsv b/papers/lsst/Photometric/CRACO/Smeared_and_zFrac.ecsv similarity index 100% rename from papers/lsst/Photometric/Smeared_and_zFrac.ecsv rename to papers/lsst/Photometric/CRACO/Smeared_and_zFrac.ecsv diff --git a/papers/lsst/Photometric/Spectroscopic.ecsv b/papers/lsst/Photometric/CRACO/Spectroscopic.ecsv similarity index 100% rename from papers/lsst/Photometric/Spectroscopic.ecsv rename to papers/lsst/Photometric/CRACO/Spectroscopic.ecsv diff --git a/papers/lsst/Photometric/zFrac.ecsv b/papers/lsst/Photometric/CRACO/zFrac.ecsv similarity index 100% rename from papers/lsst/Photometric/zFrac.ecsv rename to papers/lsst/Photometric/CRACO/zFrac.ecsv diff --git a/papers/lsst/Photometric/README.txt b/papers/lsst/Photometric/README.txt new file mode 100644 index 00000000..24726475 --- /dev/null +++ b/papers/lsst/Photometric/README.txt @@ -0,0 +1,25 @@ +The scripts in this folder were written by Bryce Smith, +with minor adaptations by C.W. James. + +The intention is to evaluate the effect of photometric redshifts +on H0 estimation using a simple 1D scan. + +The order of operation is: + +1: python create_fake_surveys.py + +This generates fake surveys for CRACO and MeerTRAP +For each, there are four: base, with mag limit, with photometric smearing, and with both. + +2: run_H0_slice.py + +This runs a slice through H0 over all surveys. Data are saved in directory H0. +All eight surveys (MeerTRAP and CRACO) are expected to be run at the same time + +python run_H0_slice.py -n 101 --min=50 --max=100 -f CRACO/Smeared CRACO/zFrac CRACO/Spectroscopic CRACO/Smeared_and_zFrac MeerTRAP/Smeared MeerTRAP/zFrac MeerTRAP/Spectroscopic MeerTRAP/Smeared_and_zFrac + +3: run python plot_h0_slice +generates the figure H0 scan_linear + +4: run python plot_2D_grids.py +Generates plots of the 2D grids for each fake survey diff --git a/papers/lsst/Photometric/create_fake_survey.py b/papers/lsst/Photometric/create_fake_survey.py index e66512c0..d3e36453 100644 --- a/papers/lsst/Photometric/create_fake_survey.py +++ b/papers/lsst/Photometric/create_fake_survey.py @@ -1,5 +1,9 @@ import os -import statistics +from matplotlib import pyplot as plt +from numpy import random +import numpy as np +from matplotlib import pyplot as plt +import importlib.resources as resources from astropy.cosmology import Planck18 from zdm import cosmology as cos @@ -12,40 +16,29 @@ from zdm import io from zdm import optical as opt from zdm import states - -from matplotlib import pyplot as plt -from numpy import random -import numpy as np from zdm import survey -from matplotlib import pyplot as plt -import importlib.resources as resources -def create_fake_survey(smearing=False): + +def create_fake_survey(smearing=False,Survey="CRACO"): + """ + Creates four fake survey files + """ sdir = str(resources.files('zdm').joinpath('data/Surveys')) - opdir="./" # directory to place fake surveys in. Here! - Prefix="""# %ECSV 1.0 -# --- -# datatype: -# - {name: TNS, datatype: string} -# - {name: DM, datatype: float64} -# - {name: RA, datatype: string} -# - {name: DEC, datatype: string} -# - {name: Z, datatype: float64} -# - {name: SNR, datatype: float64} -# - {name: WIDTH, datatype: float64} -# - {name: Gl, unit: deg, datatype: float64} -# - {name: Gb, unit: deg, datatype: float64} -# - {name: DMG, datatype: float64} -# - {name: FBAR, datatype: float64} -# - {name: BW, datatype: float64} -# meta: !!omap -# - {survey_data: '{"observing": {"NORM_FRB": 17,"TOBS": 64.68,"MAX_IW": 8, "MAXWMETH": 2""" -# we need to split the obs string into two so we can insert the zfraction as required - Suffix="""}, -# "telescope": {"BEAM": "CRACO_900", "DMMASK": "craco_900_mask.npy", -# "DIAM": 12.0, "NBEAMS": 1, "NBINS": 5, "FBAR": 906, -# "TRES": 13.8, "FRES": 1.0, "THRESH": 1.01}}'}\n""" + if Survey == "CRACO": + Prefix,Suffix = load_craco_text() + name=['CRAFT_CRACO_900'] + opdir="./CRACO/" # directory to place fake surveys in. Here! + elif Survey == "MeerTRAP": + Prefix,Suffix = load_meertrap_text() + name=['MeerTRAPcoherent'] + opdir="./MeerTRAP/" # directory to place fake surveys in. Here! + else: + raise ValueError("Survey ",Survey," not recognised") + + # make output directories + if not os.path.isdir(opdir): + os.mkdir(opdir) #param_dict={'sfr_n': 0.21, 'alpha': 0.11, 'lmean': 2.18, 'lsigma': 0.42, 'lEmax': 41.37, # 'lEmin': 39.47, 'gamma': -1.04, 'H0': 70.23, 'halo_method': 0, 'sigmaDMG': 0.0, 'sigmaHalo': 0.0,'lC': -7.61} @@ -53,7 +46,7 @@ def create_fake_survey(smearing=False): # use default state state=states.load_state(case="HoffmannHalo25",scat=None,rep=None) - name=['CRAFT_CRACO_900'] + ss,gs=loading.surveys_and_grids(survey_names=name,repeaters=False,init_state=state,sdir=sdir) gs=gs[0] @@ -110,7 +103,12 @@ def create_fake_survey(smearing=False): fp.close() frac_path = str(resources.files('zdm').joinpath('../papers/lsst/Data')) - fz=np.load(frac_path+"/fz_24.7.npy")[0:500] + + if Survey == "CRACO": + fz=np.load(frac_path+"/fz_24.7.npy")[0:500] + elif Survey == "MeerTRAP": + fz=np.load(frac_path+"/fz_27.5.npy")[0:500] + zs=np.load(frac_path+"/zvals.npy")[0:500] fp=open(opdir+"zFrac.ecsv","w+") @@ -158,4 +156,68 @@ def create_fake_survey(smearing=False): fp1.close() -create_fake_survey(True) +def load_meertrap_text(): + """ + returns CRACO prefixes and suffixes + """ + + + Prefix="""# %ECSV 1.0 +# --- +# datatype: +# - {name: TNS, datatype: string} +# - {name: DM, datatype: float64} +# - {name: RA, datatype: string} +# - {name: DEC, datatype: string} +# - {name: Z, datatype: float64} +# - {name: SNR, datatype: float64} +# - {name: WIDTH, datatype: float64} +# - {name: Gl, unit: deg, datatype: float64} +# - {name: Gb, unit: deg, datatype: float64} +# - {name: DMG, datatype: float64} +# - {name: FBAR, datatype: float64} +# - {name: BW, datatype: float64} +# meta: !!omap +# - {survey_data: '{"observing": {"NORM_FRB": 1,"TOBS": 317.5""" +# we need to split the obs string into two so we can insert the zfraction as required + Suffix="""}, +# "telescope": {"BEAM": "MeerTRAP_coherent_log", +# "BTHRESH": 0.25,"NBEAMS": 1,"NBINS": 5, +# "FRES":0.836,"THRESH":0.069, +# "TRES": 0.30624, "FBAR":1284}}'}\n""" + + return Prefix, Suffix + +def load_craco_text(): + """ + returns CRACO prefixes and suffixes + """ + + + Prefix="""# %ECSV 1.0 +# --- +# datatype: +# - {name: TNS, datatype: string} +# - {name: DM, datatype: float64} +# - {name: RA, datatype: string} +# - {name: DEC, datatype: string} +# - {name: Z, datatype: float64} +# - {name: SNR, datatype: float64} +# - {name: WIDTH, datatype: float64} +# - {name: Gl, unit: deg, datatype: float64} +# - {name: Gb, unit: deg, datatype: float64} +# - {name: DMG, datatype: float64} +# - {name: FBAR, datatype: float64} +# - {name: BW, datatype: float64} +# meta: !!omap +# - {survey_data: '{"observing": {"NORM_FRB": 17,"TOBS": 64.68,"MAX_IW": 8, "MAXWMETH": 2""" +# we need to split the obs string into two so we can insert the zfraction as required + Suffix="""}, +# "telescope": {"BEAM": "CRACO_900", "DMMASK": "craco_900_mask.npy", +# "DIAM": 12.0, "NBEAMS": 1, "NBINS": 5, "FBAR": 906, +# "TRES": 13.8, "FRES": 1.0, "THRESH": 1.01}}'}\n""" + + return Prefix, Suffix + +create_fake_survey(True,Survey="CRACO") +create_fake_survey(True,Survey="MeerTRAP") diff --git a/papers/lsst/Photometric/plot_2dgrids.py b/papers/lsst/Photometric/plot_2dgrids.py index 8fb4808f..23897daa 100644 --- a/papers/lsst/Photometric/plot_2dgrids.py +++ b/papers/lsst/Photometric/plot_2dgrids.py @@ -20,7 +20,7 @@ import matplotlib.pyplot as plt import time -def main(): +def main(Survey="CRACO",zmax=3,DMmax=3000): """ Plots 2D zDM grids """ @@ -28,10 +28,12 @@ def main(): state=states.load_state(case="HoffmannHalo25",scat=None,rep=None) sdir = resources.files('zdm').joinpath('../papers/lsst/Photometric') names = ["Spectroscopic","Smeared","zFrac","Smeared_and_zFrac"] + for i,name in enumerate(names): + names[i] = Survey+"/"+name xlabels = ["$z_{\\rm spec}$","$z_{\\rm photo}$","$z_{\\rm spec}$","$z_{\\rm photo}$"] ss,gs = loading.surveys_and_grids( survey_names=names,repeaters=False,init_state=state,sdir=sdir) - plot_grids(gs,ss,"./",xlabels) + plot_grids(gs,ss,"./",xlabels,DMmax=DMmax,zmax=zmax) #============================================================================== @@ -47,7 +49,7 @@ def main(): outdir = output directory val = parameter value for this grid """ -def plot_grids(grids, surveys, outdir,xlabels): +def plot_grids(grids, surveys, outdir,xlabels,zmax=3,DMmax=3000): for i,g in enumerate(grids): s = surveys[i] zvals=[] @@ -78,9 +80,10 @@ def plot_grids(grids, surveys, outdir,xlabels): FRBDMs=frbdmvals, FRBZs=frbzvals, Aconts=[0.01, 0.1, 0.5], - zmax=3.0, - DMmax=3000, + zmax=zmax, + DMmax=DMmax, # DMlines=nozlist, ) -main() +main(Survey="CRACO") +main(Survey="MeerTRAP",zmax=4,DMmax=4000) diff --git a/papers/lsst/Photometric/plot_H0_slice.py b/papers/lsst/Photometric/plot_H0_slice.py index d1b7d10b..c28e9dd7 100644 --- a/papers/lsst/Photometric/plot_H0_slice.py +++ b/papers/lsst/Photometric/plot_H0_slice.py @@ -28,14 +28,17 @@ 'size' : defaultsize} matplotlib.rc('font', **font) -def main(): +def main(istart=0): """ Main routine to create plots and extract characteristic parameters + + istart tells us which count to begin on """ - ll_lists=np.load("ll_lists.npy") - vals = np.load("h0vals.npy") + ll_lists=np.load("H0/ll_lists.npy") + vals = np.load("H0/h0vals.npy") nh,ns = ll_lists.shape + ns = 4 # hard-code to plot CRACO only! plt.figure() linestyles=["-","--",":","-."] @@ -48,7 +51,7 @@ def main(): FWHM=[] for i in np.arange(ns): - lls = ll_lists[:, i] + lls = ll_lists[:, i+istart] lls[lls < -1e10] = -np.inf lls[np.argwhere(np.isnan(lls))] = -np.inf @@ -59,8 +62,14 @@ def main(): lls=10**lls index1=np.where(lls>=0.5)[0][0] index2=np.where(lls>=0.5)[0][-1] - root1=vals[index1-1]-(0.5-lls[index1-1])*(vals[index1]-vals[index1-1])/(lls[index1]-lls[index1-1]) - root2=vals[index2]-(0.5-lls[index2])*(vals[index2+1]-vals[index2])/(lls[index2+1]-lls[index2]) + + root1 = vals[index1-1] * (lls[index1]-0.5)/(lls[index1]-lls[index1-1])\ + + vals[index1] * (0.5-lls[index1-1])/(lls[index1]-lls[index1-1]) + root2 = vals[index2+1] * (lls[index2]-0.5)/(lls[index2]-lls[index2+1])\ + + vals[index2] * (0.5-lls[index2+1])/(lls[index2]-lls[index2+1]) + + #root1=vals[index1-1]-(0.5-lls[index1-1])*(vals[index1]-vals[index1-1])/(lls[index1]-lls[index1-1]) + #root2=vals[index2]-(0.5-lls[index2])*(vals[index2+1]-vals[index2])/(lls[index2+1]-lls[index2]) FWHM.append(root2-root1) # plt.figure() # plt.clf() @@ -93,11 +102,16 @@ def main(): #plt.legend(loc='upper left') plt.legend(fontsize=10) plt.tight_layout() - plt.savefig("H0_scan_linear.png") + if istart==0: + plt.savefig("H0_scan_linear.png") + else: + plt.savefig("MeerKAT_H0_scan_linear.png") percentage=(FWHM/FWHM[0]-1)*100 for i,name in enumerate(s_names): print(name," FWHM is ",FWHM[i]," frac is ",percentage[i]) #print("FWHM:Spectroscopic,Photometric,zFrac,Photometric+zfrac\n",FWHM,percentage) - -main() +print("Results for CRACO") +main(istart=0) +print("\n\nResults for MeerTRAP") +main(istart=4) diff --git a/papers/lsst/Photometric/run_H0_slice.py b/papers/lsst/Photometric/run_H0_slice.py index 319a73ed..8c724fa8 100644 --- a/papers/lsst/Photometric/run_H0_slice.py +++ b/papers/lsst/Photometric/run_H0_slice.py @@ -23,7 +23,7 @@ def main(): """ run with: - python run_H0_slice.py -n 10 --min=50 --max=100 -f Smeared zFrac Spectroscopic Smeared_and_zFrac + python run_H0_slice.py -n 10 --min=50 --max=100 -f CRACO/Smeared CRACO/zFrac CRACO/Spectroscopic CRACO/Smeared_and_zFrac MeerTRAP/Smeared MeerTRAP/zFrac MeerTRAP/Spectroscopic MeerTRAP/Smeared_and_zFrac """ t0 = time.time() @@ -60,7 +60,7 @@ def main(): # state.update_param('halo_method', 1) # state.update_param(args.param, vals[0]) - outdir = 'cube/' + 'H0' + '/' + outdir = 'H0/' if not os.path.exists(outdir): os.makedirs(outdir) @@ -76,8 +76,8 @@ def main(): ll_lists = np.asarray(ll_lists) - np.save("ll_lists.npy",ll_lists) - np.save("h0vals.npy",vals) + np.save(outdir+"ll_lists.npy",ll_lists) + np.save(outdir+"h0vals.npy",vals) #============================================================================== """ diff --git a/papers/lsst/Photometric/run_slice.py b/papers/lsst/Photometric/run_slice.py index 8c51482d..e804c985 100644 --- a/papers/lsst/Photometric/run_slice.py +++ b/papers/lsst/Photometric/run_slice.py @@ -15,7 +15,12 @@ import matplotlib.pyplot as plt import time + def main(): + """ + Run with: + python run_H0_slice.py -n 91 --min=60 --max=90 -f CRACO/Spectroscopic CRACO/Smeared CRACO/zFrac CRACO/Smeared_and_zFrac MeerTRAP/Spectroscopic MeerTRAP/Smeared MeerTRAP/zFrac MeerTRAP/Smeared_and_zFrac + """ t0 = time.time() parser = argparse.ArgumentParser() diff --git a/zdm/data/Surveys/MeerTRAPcoherent.ecsv b/zdm/data/Surveys/MeerTRAPcoherent.ecsv index accc2daf..97ea8062 100644 --- a/zdm/data/Surveys/MeerTRAPcoherent.ecsv +++ b/zdm/data/Surveys/MeerTRAPcoherent.ecsv @@ -18,7 +18,7 @@ # - {survey_data: '{"observing": {"NORM_FRB": 1,"TOBS": 317.5}, # "telescope": {"BEAM": "MeerTRAP_coherent_log", # "BTHRESH": 0.25,"NBEAMS": 1,"NBINS": 5, -# "MAX_IDT": 1078, "FRES":0.836,"THRESH":0.069, +# "FRES":0.836,"THRESH":0.069, # "TRES": 0.30624, "FBAR":1284}}'} # schema: astropy-2.0 TNS BW DM DMG FBAR FRES NREP SNR SNRTHRESH THRESH TRES WIDTH Z diff --git a/zdm/grid.py b/zdm/grid.py index 5b3683df..be1ef34e 100644 --- a/zdm/grid.py +++ b/zdm/grid.py @@ -730,10 +730,12 @@ def GenMCSample(self, N, Poisson=False): # Regen if the survey would not find this FRB frb = self.GenMCFRB(Emax_boost) + # This is a pretty naive method of generation. - while frb[1] > self.survey.max_dm: - print("Regenerating MC FRB with too high DM ",frb[1],self.survey.max_dm) - frb = self.GenMCFRB(Emax_boost) + if self.survey.max_dmeg is not None: + while frb[1] > self.survey.max_dm: + print("Regenerating MC FRB with too high DM ",frb[1],self.survey.max_dm) + frb = self.GenMCFRB(Emax_boost) sample.append(frb) diff --git a/zdm/survey.py b/zdm/survey.py index 11e4e67d..14a707f3 100644 --- a/zdm/survey.py +++ b/zdm/survey.py @@ -1221,8 +1221,10 @@ def calc_max_dm(self): max_dmeg = max_dm - np.median(self.DMhalos + self.DMGs) max_idm = np.where(self.dmvals < max_dmeg)[0][-1] self.max_idm = max_idm + self.max_dmeg = max_dmeg else: self.max_idm = None + self.max_dmeg = None def get_efficiency_from_wlist(self,wlist,plist, model="Quadrature", From d4ba49dcafe32c050694eeca5182625e70b3a044 Mon Sep 17 00:00:00 2001 From: Clancy James Date: Wed, 18 Feb 2026 12:47:41 +0800 Subject: [PATCH 19/35] added updates for CASATTA re ATNF 2030 meeting --- papers/Casatta/DSA_1600.ecsv | 16 ++ papers/Casatta/compare_dsa_casatta.py | 178 +++++++++++++++++++++++ papers/Casatta/gen_atnf_plot.py | 77 ++++++++++ papers/Casatta/plot_casatta.py | 4 + papers/Casatta/sim_casatta.py | 8 +- papers/lsst/Photometric/plot_H0_slice.py | 4 +- 6 files changed, 282 insertions(+), 5 deletions(-) create mode 100644 papers/Casatta/DSA_1600.ecsv create mode 100644 papers/Casatta/compare_dsa_casatta.py create mode 100644 papers/Casatta/gen_atnf_plot.py diff --git a/papers/Casatta/DSA_1600.ecsv b/papers/Casatta/DSA_1600.ecsv new file mode 100644 index 00000000..56087552 --- /dev/null +++ b/papers/Casatta/DSA_1600.ecsv @@ -0,0 +1,16 @@ +# %ECSV 1.0 +# --- +# datatype: +# - {name: TNS, datatype: string} +# - {name: DM, datatype: float64} +# - {name: DMG, unit: pc / cm3, datatype: float64} +# - {name: SNR, datatype: float64} +# - {name: Z, datatype: float64} +# meta: !!omap +# - {survey_data: '{"observing": {"MAX_LOC_DMEG": -1}, +# "telescope": {"BMETHOD": 0, "BTHRESH": 0.5, "DIAM": 6.0, +# "NBEAMS": 1,"NBINS": 5, "FBAR": 1350.0, "FRES": 0.244141, +# "TRES": 0.262144, "SNRTHRESH": 8.5, "BW": 1300.0, "THRESH": 0.013}}'} +# schema: astropy-2.0 +TNS DM DMG SNR Z +DUMMY 491.554 37.3 133.33 -1. diff --git a/papers/Casatta/compare_dsa_casatta.py b/papers/Casatta/compare_dsa_casatta.py new file mode 100644 index 00000000..eccab508 --- /dev/null +++ b/papers/Casatta/compare_dsa_casatta.py @@ -0,0 +1,178 @@ +""" +Runs a simulation of DSA 1600, compartes that to CASATTA N... +""" +from scipy.interpolate import interp1d +import pandas as pd +import numpy as np +import importlib.resources as resources +import copy +import scipy.constants as constants +from matplotlib import pyplot as plt + +from zdm import states +from zdm import misc_functions as mf +from zdm import grid as zdm_grid +from zdm import survey +from zdm import pcosmic + + +import matplotlib + +defaultsize=14 +ds=4 +font = {'family' : 'Helvetica', + 'weight' : 'normal', + 'size' : defaultsize} +matplotlib.rc('font', **font) + +def main(): + """ + + """ + # does exactly the same as "sim_casatta.py", just picks out the + # specific cases we want + + # loads + #df = read_casatta_params() + #nsims,ncols = df.shape + + + + ##### SIMULATES DSA 1600 as per CASATTA ######3 + + # state. Does not use updated scattering, because it takes a long time! + state = states.load_state("HoffmannHalo25")#scat="updated",rep=None) + + zDMgrid, zvals, dmvals = mf.get_zdm_grid( + state, new=True, plot=False, method='analytic', + datdir=resources.files('zdm').joinpath('GridData')) + + # we can keep this constant - it smears DM due to host DM + mask = pcosmic.get_dm_mask(dmvals, (state.host.lmean, state.host.lsigma), zvals, plot=False) + + renorm = get_constant(state,zDMgrid, zvals, dmvals, mask) + + sdir="." + survey_name = "DSA_1600" + + s = survey.load_survey(survey_name, state, dmvals, zvals=zvals, sdir=sdir) + + g = zdm_grid.Grid(s, copy.deepcopy(state), zDMgrid, zvals, dmvals, mask, wdist=True) + + + daily = np.sum(g.rates)* 10**state.FRBdemo.lC *renorm + + pz = np.sum(g.rates,axis=1)* 10**state.FRBdemo.lC *renorm + pdm = np.sum(g.rates,axis=0)* 10**state.FRBdemo.lC *renorm + dz1 = zvals[1]-zvals[0] + print("Daily DSA rate is ",daily) + ###### reads CASATTA ##### + + df = pd.read_csv("CASATTA MFAA SKA2 FRB estimates.csv") + + dailys = np.load("dailys.npy") + pzs = np.load("pzs.npy")*renorm + pdms = np.load("pdms.npy")*renorm + zvals2 = np.load("zvals.npy") + dmvals = np.load("dmvals.npy") + threshs = np.load("threshs.npy") + + + + # selects which casatta plot to use + OK1=np.where(df["FrequencyMHz"]==600) + OK2=np.where(df["FWHM_deg"]==120) + OK=np.intersect1d(OK1,OK2) + + dz2 = zvals2[1]-zvals2[0] + + # selects which casatta we want + ic = 13 + print(df["Array_name"][ic]," has daily rate ",dailys[ic]) + # plots! + + optdir = str(resources.files('zdm').joinpath('data/optical'))+"/" + + yf = np.load(optdir+"fz_23.0.npy") + xf = np.load(optdir+"zvals.npy") + itp = interp1d(xf,yf) + hfracs = itp(zvals) + + yf2 = np.load(optdir+"fz_24.7.npy") + xf2 = np.load(optdir+"zvals.npy") + itp2 = interp1d(xf2,yf2) + hfracs2 = itp2(zvals) + + + yf3 = np.load(optdir+"fz_27.5.npy") + xf3 = np.load(optdir+"zvals.npy") + itp3 = interp1d(xf3,yf3) + hfracs3 = itp3(zvals) + + plt.figure() + plt.xlim(0,5) + plt.ylim(0,550) + # multiplies by z-bin width + dz = zvals[1]-zvals[0] + plt.xlabel("z") + plt.ylabel("p(z) [FRBs / day / z]") + #plt.ylim(1e-1,1e7) + plt.plot(zvals,pz/dz1,label="DSA 1600: FRBs",color="red") + plt.fill_between(zvals,pz/dz1*hfracs,label=" hosts: DECaLS",color="red",alpha=0.5) + print("DSA hosts: ",np.sum(pz*hfracs)) + plt.plot(zvals,pzs[ic,:]/dz2,label="CASATTA 4000: FRBs",color="blue") + plt.fill_between(zvals,pzs[ic,:]/dz2*hfracs3,label=" hosts: LSST",color="blue",alpha=0.5) + print("CASATTA hosts: ",np.sum(pzs[ic,:]*hfracs3)) + plt.legend() + plt.tight_layout() + plt.savefig("dsa_pz_30_vs_27.5.png") + plt.close() + + + + plt.figure() + plt.xlim(0,5) + plt.ylim(0,550) + # multiplies by z-bin width + dz = zvals[1]-zvals[0] + plt.xlabel("z") + plt.ylabel("p(z) [FRBs / day / z]") + #plt.ylim(1e-1,1e7) + plt.plot(zvals,pz/dz1,label="DSA 1600: FRBs",color="red") + plt.fill_between(zvals,pz/dz1*hfracs2,label="DSA 1600: hosts",color="red",alpha=0.5) + print("DSA hosts: ",np.sum(pz*hfracs2)) + plt.plot(zvals,pzs[ic,:]/dz2,label="CASATTA 4000: FRBs",color="blue") + plt.fill_between(zvals,pzs[ic,:]/dz2*hfracs2,label="CASATTA 4000: hosts",color="blue",alpha=0.5) + print("CASATTA hosts: ",np.sum(pzs[ic,:]*hfracs2)) + plt.legend() + plt.tight_layout() + plt.savefig("dsa_pz_both_24.7.png") + plt.close() + + +def get_constant(state,zDMgrid, zvals, dmvals, mask): + """ + gets a normalising constant for this state + + Args: + df: dataframe containing info for this version of casatta + state: zdm state object + zDMgrid: underlying zDM grid giving p(DMcosmic|z) + zvals: redshift values of grid + dmvals: DM values of grid + mask: DM smearing mask for grid based on DMhost + """ + # I am here choosing to renomalise by the CRAFT ICS 892 MHz rates + #norm_survey = "CRAFT_class_I_and_II" + norm_survey = "CRAFT_ICS_892" + s = survey.load_survey(norm_survey, state, dmvals, zvals=zvals) + g = zdm_grid.Grid(s, copy.deepcopy(state), zDMgrid, zvals, dmvals, mask, wdist=True) + + predicted = np.sum(g.rates) * s.TOBS * 10**state.FRBdemo.lC + observed = s.NORM_FRB + + renorm = observed/predicted + print("Calculated renomalisation constant as ",renorm) + return renorm + +main() diff --git a/papers/Casatta/gen_atnf_plot.py b/papers/Casatta/gen_atnf_plot.py new file mode 100644 index 00000000..c374307e --- /dev/null +++ b/papers/Casatta/gen_atnf_plot.py @@ -0,0 +1,77 @@ + +import numpy as np +from matplotlib import pyplot as plt + +import matplotlib + +defaultsize=14 +ds=4 +font = {'family' : 'Helvetica', + 'weight' : 'normal', + 'size' : defaultsize} +matplotlib.rc('font', **font) + + + +def main(): + """ + plots casatta simulation results + """ + + # this factor did NOT multiply pzs and pdms because I'm an idiot and forgot + # daily rates are correct + RENORM = 0.177 + + df = read_casatta_params() + nsims,ncols = df.shape + + + # selects which casatta plot to use + OK1=np.where(df["FrequencyMHz"]==600) + OK2=np.where(df["FWHM_deg"]==120) + OK=np.intersect1d(OK1,OK2) + + + dailys = np.load("dailys.npy")[OK] + pzs = np.load("pzs.npy")[OK,:] + pdms = np.load("pdms.npy")[OK,:] + pzs *= RENORM + pdms *= RENORM + zvals = np.load("zvals.npy") + dmvals = np.load("dmvals.npy") + threshs = np.load("threshs.npy")[OK] + + + # compares estimates from nominal figure of merit + FOM = threshs**-1.5 * df["FWHM_deg"][OK]**2 + + plt.figure() + plt.xlabel("FOM [FWHM$^2$ (Jy ms)$^{-1.5}$]") + plt.ylabel("Daily rate") + plt.xscale("log") + plt.yscale("log") + plt.scatter(FOM,dailys,label="CASATTA 600 MHz, FWHM=120 deg") + plt.ylim(1e-2,1e8) + plt.xlim(1,1e8) + + plt.plot([1e-2,1e8],[3e-4,3e6],color="black",label="1-1 line",linestyle="--") + plt.legend(loc="upper left") + plt.tight_layout() + plt.savefig("FOM_ATNF.png") + plt.close() + + +def read_casatta_params(infile="CASATTA MFAA SKA2 FRB estimates.csv"): + """ + Reads in casatta parameters + """ + + import pandas as pd + df = pd.read_csv(infile) + + return df + + +main() + + diff --git a/papers/Casatta/plot_casatta.py b/papers/Casatta/plot_casatta.py index 5fdb2b15..45d53aed 100644 --- a/papers/Casatta/plot_casatta.py +++ b/papers/Casatta/plot_casatta.py @@ -17,6 +17,10 @@ def main(): """ plots casatta simulation results """ + + # this factor did NOT multiply pzs and pdms because I'm an idiot and forgot + # daily rates are correct + df = read_casatta_params() nsims,ncols = df.shape diff --git a/papers/Casatta/sim_casatta.py b/papers/Casatta/sim_casatta.py index 5272e941..81cc2d8d 100644 --- a/papers/Casatta/sim_casatta.py +++ b/papers/Casatta/sim_casatta.py @@ -49,14 +49,14 @@ def main(): for isim in np.arange(nsims): daily,pz,pdm,thresh = sim_casatta(df.iloc[isim],state,zDMgrid,zvals,dmvals,mask) - dailys[isim]=daily - pzs[isim,:]=pz - pdms[isim,:]=pdm + dailys[isim]=daily*renorm + pzs[isim,:]=pz*renorm + pdms[isim,:]=pdm*renorm threshs[isim] = thresh print("Done simulation ",isim, " of ", nsims,", daily rate ",daily*renorm) # modifies rates according to expectations - dailys *= renorm + np.save("threshs.npy",threshs) np.save("dailys.npy",dailys) np.save("pzs.npy",pzs) diff --git a/papers/lsst/Photometric/plot_H0_slice.py b/papers/lsst/Photometric/plot_H0_slice.py index c28e9dd7..d592174b 100644 --- a/papers/lsst/Photometric/plot_H0_slice.py +++ b/papers/lsst/Photometric/plot_H0_slice.py @@ -65,9 +65,11 @@ def main(istart=0): root1 = vals[index1-1] * (lls[index1]-0.5)/(lls[index1]-lls[index1-1])\ + vals[index1] * (0.5-lls[index1-1])/(lls[index1]-lls[index1-1]) + root2 = vals[index2+1] * (lls[index2]-0.5)/(lls[index2]-lls[index2+1])\ + vals[index2] * (0.5-lls[index2+1])/(lls[index2]-lls[index2+1]) - + + #root1=vals[index1-1]-(0.5-lls[index1-1])*(vals[index1]-vals[index1-1])/(lls[index1]-lls[index1-1]) #root2=vals[index2]-(0.5-lls[index2])*(vals[index2+1]-vals[index2])/(lls[index2+1]-lls[index2]) FWHM.append(root2-root1) From b32743823101c2e2e89fd09d84f32c8b347cbb56 Mon Sep 17 00:00:00 2001 From: Clancy James Date: Wed, 18 Feb 2026 12:48:32 +0800 Subject: [PATCH 20/35] oops, missed one addition --- papers/lsst/sim_pz.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/papers/lsst/sim_pz.py b/papers/lsst/sim_pz.py index 07a1c59f..9f5afc52 100644 --- a/papers/lsst/sim_pz.py +++ b/papers/lsst/sim_pz.py @@ -56,6 +56,8 @@ def main(): Rlim0 = 19.8 # existing magnitude limits Rlim1 = 24.7 Rlim2 = 27.5 + Rlim3 = 23.0 #decals + names=['CRAFT_CRACO_1300','MeerTRAPcoherent','SKA_mid'] labels=["ASKAP CRACO", "MeerKAT","SKA-Mid"] @@ -96,9 +98,11 @@ def main(): fz0 = np.zeros([nz]) fz1 = np.zeros([nz]) fz2 = np.zeros([nz]) + fz3 = np.zeros([nz]) iz0 = np.where(Rbars < Rlim0)[-1] iz1 = np.where(Rbars < Rlim1)[-1] iz2 = np.where(Rbars < Rlim2)[-1] + iz3 = np.where(Rbars < Rlim3)[-1] for i,z in enumerate(zvals): if z < Rzvals[0]: @@ -119,9 +123,12 @@ def main(): fz0[i] = norm.cdf(Rlim0) fz1[i] = norm.cdf(Rlim1) fz2[i] = norm.cdf(Rlim2) + fz3[i] = norm.cdf(Rlim3) np.save(optdir+"fz_19.8.npy",fz0) np.save(optdir+"fz_24.7.npy",fz1) np.save(optdir+"fz_27.5.npy",fz2) + np.save(optdir+"fz_23.0.npy",fz3) + np.save(opdir+"Rhist.npy",Rhist) np.save(opdir+"Rvals.npy",Rvals) np.save(opdir+"Rbars.npy",Rbars) From 6dc45405a07ce0c1f47002c82690797a0bfc918b Mon Sep 17 00:00:00 2001 From: Clancy James Date: Tue, 3 Mar 2026 07:46:17 +0800 Subject: [PATCH 21/35] Cosmetic changes for first full draft of paper --- papers/lsst/make_host_z_mag_plot.py | 48 ++-- papers/pathpriors/compare_posteriors.py | 20 +- papers/pathpriors/fit_loudas_model.py | 52 +++- papers/pathpriors/fit_simple_model.py | 59 +++- .../{pU_g_mr => pO_g_mr}/fit_pogmr.py | 20 +- .../pu_mr_vs_mag_CRAFT_VLT_FORS2_r.csv | 0 papers/pathpriors/plot_host_models.py | 138 ++++++++++ papers/pathpriors/plot_marnoch_model.py | 2 +- papers/pathpriors/simple_systematics.py | 251 ++++++++++++++++++ zdm/optical.py | 7 +- zdm/optical_params.py | 4 +- 11 files changed, 547 insertions(+), 54 deletions(-) rename papers/pathpriors/{pU_g_mr => pO_g_mr}/fit_pogmr.py (80%) rename papers/pathpriors/{pU_g_mr => pO_g_mr}/pu_mr_vs_mag_CRAFT_VLT_FORS2_r.csv (100%) create mode 100644 papers/pathpriors/plot_host_models.py create mode 100644 papers/pathpriors/simple_systematics.py diff --git a/papers/lsst/make_host_z_mag_plot.py b/papers/lsst/make_host_z_mag_plot.py index 6d0851ed..b0237f42 100644 --- a/papers/lsst/make_host_z_mag_plot.py +++ b/papers/lsst/make_host_z_mag_plot.py @@ -3,6 +3,8 @@ Adds points corresponding to known hosts +We actually do this for best-fitting models from the PATH paper + """ #standard Python imports @@ -41,9 +43,16 @@ def make_zmr_plots(): os.mkdir(opdir) # model 1: simple model - model1 = opt.simple_host_model() - absprior = [0,0,0.0705, 0.839, 0.328, 0.001,0,0,0,0] - model1.init_args(absprior) + opstate = op.OpticalState() + # sets optical state to use simple linear interpolation + opstate.simple.AbsModelID = 1 # linear interpolation + opstate.simple.AppModelID = 1 # include k-correction + opstate.simple.NModelBins = 6 + opstate.simple.Absmin = -25 + opstate.simple.Absmax = -15 + model1 = opt.simple_host_model(opstate) + xbest = np.load("../pathpriors/simple_output/best_fit_params.npy") + model1.init_args(xbest) # this is from an initial estimate. Currently, no way to enter this into the opstate. To do. @@ -56,18 +65,24 @@ def make_zmr_plots(): model3=opt.loudas_model() model3.init_args(1.) + xbest = np.load("../pathpriors/loudas_output/best_fit_params.npy") + model4=opt.loudas_model() + model4.init_args(xbest) + # model from Lachlan - model4=opt.marnoch_model() + model5=opt.marnoch_model() - models=[model1,model2,model3,model4] - labels=["simple","sfr0","sfr1","marnoch"] + models=[model1,model2,model3,model4,model5] + labels=["naive","sfr0","sfr1","loudas","marnoch"] + labels2=["(c) Naive","sfr0","sfr1","(b) Loudas25","(a) Marnoch23"] for i,model in enumerate(models): opfile = opdir+labels[i]+"_zmr.png" - make_host_plot(model,opfile) + + make_host_plot(model,labels2[i],opfile) -def make_host_plot(model,opfile): +def make_host_plot(model,label,opfile): """ generates a plot showing the magnitude and redshift of a bunch of FRB host galaxies @@ -76,7 +91,7 @@ def make_host_plot(model,opfile): opfile: string labelling the plotfile """ - nz=200 + nz=50 zmax=2 zmin = zmax/nz zvals = np.linspace(zmin,zmax,nz) @@ -131,12 +146,15 @@ def make_host_plot(model,opfile): plt.ylim(10,30) plt.xlim(0,zmax) - Rlim1=24.7 - Rlim2=27.5 - plt.plot([0,zmax],[Rlim1,Rlim1],linestyle=":",color="black") - plt.plot([0,zmax],[Rlim2,Rlim2],linestyle=":",color="black") - plt.text(0.1,Rlim1+0.2,"$m_r^{\\rm lim}=$"+str(Rlim1)) - plt.text(0.1,Rlim2+0.2,"$m_r^{\\rm lim}=$"+str(Rlim2)) + print("label is ",label) + plt.text(0.04,29,label) + + #Rlim1=24.7 + #Rlim2=27.5 + #plt.plot([0,zmax],[Rlim1,Rlim1],linestyle=":",color="black") + #plt.plot([0,zmax],[Rlim2,Rlim2],linestyle=":",color="black") + #plt.text(0.1,Rlim1+0.2,"$m_r^{\\rm lim}=$"+str(Rlim1)) + #plt.text(0.1,Rlim2+0.2,"$m_r^{\\rm lim}=$"+str(Rlim2)) plt.legend() plt.tight_layout() diff --git a/papers/pathpriors/compare_posteriors.py b/papers/pathpriors/compare_posteriors.py index d401cfc2..982cef92 100644 --- a/papers/pathpriors/compare_posteriors.py +++ b/papers/pathpriors/compare_posteriors.py @@ -1,9 +1,11 @@ """ -This file fits the simple (naive) model to CRAFT ICDS observations. +This file fits the simple (naive) model to CRAFT ICS observations. It fits a model of absolute galaxy magnitude distributions, uses zDM to predict redshifts and hence apparent magntidues, runs PATH using that prior, and tries to get priors to match posteriors. +It also geenrates host z-mr plots + """ @@ -93,7 +95,7 @@ def main(): NFRB1,AppMags1,AppMagPriors1,ObsMags1,ObsPrior1,ObsPosteriors1,PUprior1,PUobs1,sumPUprior1,sumPUobs1,frbs,dms = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) fObsPosteriors1 = on.flatten(ObsPosteriors1) - plt.scatter(fObsPosteriors2,fObsPosteriors1,label="Marnoch",marker='s') + plt.scatter(fObsPosteriors2,fObsPosteriors1,label="Marnoch23",marker='s') with open("posteriors/marnoch.txt",'w') as f: @@ -106,12 +108,13 @@ def main(): ####### Model 2: Loudas ######## model = opt.loudas_model() - model.init_args([1.49]) # best-fit arguments + xbest = np.load("loudas_output/best_fit_params.npy") + model.init_args(xbest) # best-fit arguments wrappers = on.make_wrappers(model,gs) NFRB3,AppMags3,AppMagPriors3,ObsMags3,ObsPrior3,ObsPosteriors3,PUprior3,PUobs3,sumPUprior3,sumPUobs3,frbs,dms = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) fObsPosteriors3 = on.flatten(ObsPosteriors3) - plt.scatter(fObsPosteriors2,fObsPosteriors3,label="Loudas",marker='x') + plt.scatter(fObsPosteriors2,fObsPosteriors3,label="Loudas25",marker='x') with open("posteriors/loudas.txt",'w') as f: for i,frb in enumerate(frbs): @@ -128,14 +131,17 @@ def main(): # sets optical state to use simple linear interpolation opstate.simple.AbsModelID = 1 # linear interpolation opstate.simple.AppModelID = 1 # include k-correction - opstate + opstate.simple.NModelBins = 6 + opstate.simple.Absmin = -25 + opstate.simple.Absmax = -15 model = opt.simple_host_model(opstate) # retrieve default initial arguments in vector form - x = [-2.28795519, 0., 0. , 0. , 0.11907231,0.84640048,0.99813815 , 0., 0. , 0. , 0. ] + xbest = np.load("simple_output/best_fit_params.npy") + #x = [-2.28795519, 0., 0. , 0. , 0.11907231,0.84640048,0.99813815 , 0., 0. , 0. , 0. ] # initialises best-fit arguments - model.init_args(x) + model.init_args(xbest) ############# Generate a KS-like plot showing the best fits #################### wrappers = on.make_wrappers(model,gs) diff --git a/papers/pathpriors/fit_loudas_model.py b/papers/pathpriors/fit_loudas_model.py index 50920da0..f17ff22d 100644 --- a/papers/pathpriors/fit_loudas_model.py +++ b/papers/pathpriors/fit_loudas_model.py @@ -8,6 +8,7 @@ import numpy as np from matplotlib import pyplot as plt from scipy.optimize import minimize +from scipy.stats import chi2 # imports from the "FRB" series from zdm import optical as opt @@ -88,17 +89,19 @@ def main(): np.save(opdir+"/best_fit_params.npy",x) else: # replace later - x=[1.49] + x=[3] print("using previous result of f_sfr = ",x) # initialises arguments model.init_args(x) + bestx=x[0] + xstring = f"{bestx:.1f}" outfile = opdir+"loudas_best_fit_apparent_magnitudes.png" wrappers = on.make_wrappers(model,gs) NFRB,AppMags,AppMagPriors,ObsMags,ObsPriors,ObsPosteriors,PUprior,PUobs,sumPUprior,sumPUobs,frbs,dms = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) llstat = on.calculate_likelihood_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUobs,PUprior,plotfile=outfile) - + llbest = llstat ksstat = on.calculate_ks_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,sumPUobs,sumPUprior,plotfile=outfile) print("Best-fit stats of the Loudas model are ll=",llstat," ks = ",ksstat) @@ -115,7 +118,7 @@ def main(): sumPUpriorlist = [] sumPUobslist = [] - for f_sfr in [1.49,0,1]: + for f_sfr in [bestx,0,1]: x=[f_sfr] model.init_args(x) wrappers = on.make_wrappers(model,gs) @@ -133,7 +136,7 @@ def main(): NMODELS = 3 - plotlabels=["$f_{\\rm sfr} = 1.49$","$f_{\\rm sfr} = 0$", "$f_{\\rm sfr} = 1$"] + plotlabels=["$f_{\\rm sfr} = "+xstring+"$", "$f_{\\rm sfr} = 0$", "$f_{\\rm sfr} = 1$"] plotfile = opdir+"loudas_f0_1_best_comparison.png" on.make_cumulative_plots(NMODELS,NFRBlist,AppMagslist,AppMagPriorslist,ObsMagslist,ObsPosteriorslist, PUobslist,PUpriorlist,plotfile,plotlabels,POxcut=None,onlyobs=0,abc="(b)") @@ -142,8 +145,12 @@ def main(): NSFR=31 stats = np.zeros([NSFR]) SFRs = np.linspace(0,3,NSFR) + pvalues = np.zeros([NSFR]) + dlls = np.zeros([NSFR]) + for istat,sfr in enumerate(SFRs): - + if not minimise: + break model.init_args([sfr]) wrappers = on.make_wrappers(model,gs) NFRB,AppMags,AppMagPriors,ObsMags,ObsPriors,ObsPosteriors,PUprior,\ @@ -151,13 +158,44 @@ def main(): stat = on.calculate_likelihood_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUobs, PUprior,plotfile=outfile,POxcut=POxcut) stats[istat] = stat + dll = 2.*(llbest-stat) * np.log(10) # stat returned in log base 10, needs to be natural log + p_wilks = 1.-chi2.cdf(dll,1) + pvalues[istat] = p_wilks + dlls[istat] = dll + + if minimise: + # save data if doing this for the first time + np.save(opdir+"/llk.npy",stats) + np.save(opdir+"/pvalues.npy",pvalues) + np.save(opdir+"/dlls.npy",dlls) + else: + # else, load it + stats = np.load(opdir+"/llk.npy") + pvalues = np.load(opdir+"/pvalues.npy") + dlls = np.load(opdir+"/dlls.npy") + + # print values + for i,f in enumerate(SFRs): + print("p-value of ",f," is ",pvalues[i]) + + outfile = opdir+"scan_sfr.png" + plt.figure() - plt.plot(SFRs,stats,marker="o") + l1,=plt.plot(SFRs,stats,marker="o") plt.xlabel("$f_{\\rm sfr}$") plt.ylabel("$\\log_{10} \\mathcal{L}(f_{\\rm sfr})$") plt.xlim(0,3) - plt.ylim(48,53) + plt.ylim(44,53) + + ax2 = plt.gca().twinx() + l2,=ax2.plot(SFRs,pvalues,color="black",linestyle=":",label="p-value") + plt.yscale('log') + plt.ylabel('p-value') + plt.ylim(1e-3,1.) + + plt.legend(handles=[l1,l2],labels=["$\\log_{10} \\mathcal{L} (f_{\\rm sfr})$","p-value"],loc="lower right") + plt.tight_layout() plt.savefig(outfile) plt.close() diff --git a/papers/pathpriors/fit_simple_model.py b/papers/pathpriors/fit_simple_model.py index 4abf7c02..23154f14 100644 --- a/papers/pathpriors/fit_simple_model.py +++ b/papers/pathpriors/fit_simple_model.py @@ -12,6 +12,7 @@ import numpy as np from matplotlib import pyplot as plt from scipy.optimize import minimize +from scipy.stats import chi2 # imports from the "FRB" series from zdm import optical as opt @@ -75,7 +76,7 @@ def main(): # set to e.g. 0.9 to reject FRBs with lower posteriors when doing model comparisons POxcut = None - opdir = modelname+"_output/" + opdir = "simple_output/" if not os.path.exists(opdir): @@ -87,6 +88,9 @@ def main(): opstate = op.OpticalState() # sets optical state to use simple linear interpolation opstate.simple.AbsModelID = 1 + opstate.simple.NModelBins = 6 + opstate.simple.Absmin = -25 + opstate.simple.Absmax = -15 # sets up initial bounds on variables if dok: @@ -114,7 +118,7 @@ def main(): args=[frblist,ss,gs,model,POxcut,istat] # set to false to just use hard-coded best fit parameters - minimise=False + minimise=True if minimise: result = minimize(on.function,x0 = x0,args=args,bounds = bounds) print("Best fit result is ",result.x) @@ -123,11 +127,8 @@ def main(): np.save(opdir+"/best_fit_params.npy",x) else: # hard-coded best fit parameters from running optimisation - if not dok: - print("Need to re-run this!") - exit() - else: - x = [-2.28795519,0.,0.,0.,0.11907231,0.84640048,0.99813815,0.,0.,0.,0.] + x = np.load(opdir+"best_fit_params.npy") + # initialises best-fit arguments model.init_args(x) @@ -142,25 +143,62 @@ def main(): print("Best-fit stats of the naive model are ll=",llstat," ks = ",ksstat) + # we determine the range of k which are compatible at 1,2,3 sigma using Wilks' theorem + # this states that 2*log(L(k)-L(k=0)) should be distributed according to a chi2 distribution + # with one degree of freedom ############ k-correction figure ############3 # we generate a plot showing the convergence on k, i.e. how/why we get a best fit - nk=21 + llbest = llstat + nk=101 kvals = np.linspace(-5,5,nk) stats = np.zeros([nk]) + pvalues = np.zeros([nk]) + dlls = np.zeros([nk]) for i,kcorr in enumerate(kvals): + if not minimise: + break x[0] = kcorr model.init_args(x) wrappers = on.make_wrappers(model,gs) NFRB,AppMags,AppMagPriors,ObsMags,ObsPriors,ObsPosteriors,PUprior,PUobs,sumPUprior,sumPUobs,frbs,dms = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) stat = on.calculate_likelihood_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUobs,PUprior) stats[i] = stat + dll = 2.*(llbest-stat) * np.log(10) # stat returned in log base 10, needs to be natural log + p_wilks = 1.-chi2.cdf(dll,1) + pvalues[i] = p_wilks + dlls[i] = dll + + if minimise: + # save data if doing this for the first time + np.save(opdir+"/llk.npy",stats) + np.save(opdir+"/pvalues.npy",pvalues) + np.save(opdir+"/kvals.npy",kvals) + np.save(opdir+"/dlls.npy",dlls) + else: + # else, load it + stats = np.load(opdir+"/llk.npy") + pvalues = np.load(opdir+"/pvalues.npy") + kvals = np.load(opdir+"/kvals.npy") + dlls = np.load(opdir+"/dlls.npy") + + for i,k in enumerate(kvals): + print("p-value of ",k," is ",pvalues[i]) plt.figure() - plt.plot(kvals,stats) + l1,=plt.plot(kvals,stats,label="$\\log_{10} \\mathcal{L} (k)$") #plt.yscale('log') plt.xlabel('$k$') plt.ylabel('$\\log_{10} \\mathcal{L} (k)$') + #plt.legend() + + ax2 = plt.gca().twinx() + l2,=ax2.plot(kvals,pvalues,color="black",linestyle=":",label="p-value") + plt.yscale('log') + plt.ylabel('p-value') + plt.ylim(1e-3,1.) + plt.legend(handles=[l1,l2],labels=["$\\log_{10} \\mathcal{L} (k)$","p-value"],loc="lower right") + plt.tight_layout() plt.savefig(opdir+'/pkvalue.png') plt.close() @@ -207,7 +245,6 @@ def main(): plt.savefig(opdir+"best_fit_absolute_magnitudes.png") plt.close() - - main() + diff --git a/papers/pathpriors/pU_g_mr/fit_pogmr.py b/papers/pathpriors/pO_g_mr/fit_pogmr.py similarity index 80% rename from papers/pathpriors/pU_g_mr/fit_pogmr.py rename to papers/pathpriors/pO_g_mr/fit_pogmr.py index 5121419f..54ce5253 100644 --- a/papers/pathpriors/pU_g_mr/fit_pogmr.py +++ b/papers/pathpriors/pO_g_mr/fit_pogmr.py @@ -64,19 +64,23 @@ print(" Legacy ",Legacy_result) print(" VLT/FORS2 ",CRAFT_result) +LpU_mr = np.array(LpU_mr) +PSpU_mr = np.array(PSpU_mr) + plt.figure() -plt.plot(df['mag'],df['PU_mr'],label="VLT/FORS2") -plt.plot(df['mag'],CRAFT_pogm,label=" (fit)",linestyle="--",color = plt.gca().lines[-1].get_color()) -plt.plot(Lmags,LpU_mr,label="Legacy surveys") -plt.plot(Lmags,Legacy_fit,label=" (fit)",linestyle="--",color = plt.gca().lines[-1].get_color()) -plt.plot(PSmags,PSpU_mr,label="Pan-STARRS") -plt.plot(PSmags,PS_fit,label=" (fit)",linestyle="--",color = plt.gca().lines[-1].get_color()) +plt.plot(df['mag'],1.-df['PU_mr'],label="VLT/FORS2") +plt.plot(df['mag'],1.-CRAFT_pogm,label=" (fit)",linestyle="--",color = plt.gca().lines[-1].get_color()) +plt.plot(Lmags,1.-LpU_mr,label="Legacy surveys") +plt.plot(Lmags,1.-Legacy_fit,label=" (fit)",linestyle="--",color = plt.gca().lines[-1].get_color()) +plt.plot(PSmags,1.-PSpU_mr,label="Pan-STARRS") +plt.plot(PSmags,1.-PS_fit,label=" (fit)",linestyle="--",color = plt.gca().lines[-1].get_color()) plt.legend() plt.xlim(15,30) +plt.ylim(0,1) plt.xlabel("$m_r$") -plt.ylabel("$p(U|m_r)$") +plt.ylabel("$p(O|m_r)$") plt.tight_layout() -plt.savefig("pUgm.png") +plt.savefig("pOgm.png") plt.close() diff --git a/papers/pathpriors/pU_g_mr/pu_mr_vs_mag_CRAFT_VLT_FORS2_r.csv b/papers/pathpriors/pO_g_mr/pu_mr_vs_mag_CRAFT_VLT_FORS2_r.csv similarity index 100% rename from papers/pathpriors/pU_g_mr/pu_mr_vs_mag_CRAFT_VLT_FORS2_r.csv rename to papers/pathpriors/pO_g_mr/pu_mr_vs_mag_CRAFT_VLT_FORS2_r.csv diff --git a/papers/pathpriors/plot_host_models.py b/papers/pathpriors/plot_host_models.py new file mode 100644 index 00000000..7535c965 --- /dev/null +++ b/papers/pathpriors/plot_host_models.py @@ -0,0 +1,138 @@ +""" +Used to generate final fitted P(m|DM) figures + +""" + +#standard Python imports +import os +import numpy as np +from matplotlib import pyplot as plt + +# imports from the "FRB" series +from zdm import optical as opt +from zdm import optical_params as op +from zdm import optical_numerics as on +from zdm import loading +from zdm import cosmology as cos +from zdm import parameters +from zdm import loading + +import astropath.priors as pathpriors + +import matplotlib + +defaultsize=14 +ds=4 +font = {'family' : 'Helvetica', + 'weight' : 'normal', + 'size' : defaultsize} +matplotlib.rc('font', **font) + +def calc_path_priors(): + """ + Loops over all ICS FRBs + """ + + opdir = "Plots/" + if not os.path.exists(opdir): + os.mkdir(opdir) + ##### performs the following calculations for the below combinations ###### + + ######## initialises optical-independent info ######## + #frblist is a hard-coded list of FRBs for which we have optical PATH data + frblist = opt.frblist + NFRB = len(frblist) + + + state = parameters.State() + cos.set_cosmology(state) + cos.init_dist_measures() + + ##### makes a plot of host priors for the simple model ###### + + # simple host model + # Case of simple host model + opstate1 = op.OpticalState() + # sets optical state to use simple linear interpolation + opstate1.simple.AbsModelID = 1 # linear interpolation + opstate1.simple.AppModelID = 1 # include k-correction + opstate1.simple.NModelBins = 6 + opstate1.simple.Absmin = -25 + opstate1.simple.Absmax = -15 + model1 = opt.simple_host_model(opstate1) + # this is from an initial estimate. Currently, no way to enter this into the opstate. To do. + xbest = np.load("simple_output/best_fit_params.npy") + model1.init_args(xbest) + + + model2=opt.loudas_model() + xbest = np.load("loudas_output/best_fit_params.npy") + model2.init_args(xbest) # best-fit arguments + + # set up basic histogram of p(mr) distribution + mrbins = np.linspace(0,40,401) + mrvals=(mrbins[:-1]+mrbins[1:])/2. + dmr = mrbins[1]-mrbins[0] + + model3 = opt.marnoch_model() + + ######### Plots apparent mag distribution for all models as function of z ####### + styles=["-",":","--","-."] + + plt.figure() + flist=[1,3] #normal distributions, and best fit + + for i,z in enumerate([0.1,1.0]): + + # simple model + pmr = model1.get_pmr_gz(mrbins,z) + pmr /= np.sum(pmr) + + if i==0: + plt.plot(mrvals,pmr/dmr,label="Naive",linestyle=styles[0]) + else: + plt.plot(mrvals,pmr/dmr,linestyle=styles[0],\ + color=plt.gca().lines[0].get_color()) + + pmr = model3.get_pmr_gz(mrbins,z) + if i==0: + plt.plot(mrvals,pmr/dmr,label = "Marnoch23",linestyle=styles[3]) + else: + plt.plot(mrvals,pmr/dmr,linestyle=styles[3],color=plt.gca().lines[1].get_color()) + + + # Loudas model dependencies + for j,fsfr in enumerate(flist): + model2.init_args(fsfr) + pmr = model2.get_pmr_gz(mrbins,z) + pmr /= np.sum(pmr) + if i==0: + plt.plot(mrvals,pmr/dmr,label = "Loudas25 ($f_{\\rm sfr}$ = "+str(fsfr)+")", + linestyle=styles[j+1]) + else: + plt.plot(mrvals,pmr/dmr,linestyle=styles[j+2],\ + color=plt.gca().lines[j+2].get_color()) + + + + plt.xlabel("Apparent magnitude $m_r$") + plt.ylabel("$P(m_r|z)$") + plt.text(17.5,0.28,"$z=0.1$") + plt.text(22,0.28,"$z=1.0$") + plt.xlim(12.5,30) + plt.ylim(0,0.4) + plt.legend(loc="upper right",ncol=2) + #plt.legend(loc=[25,0.35]) + + plt.tight_layout() + plt.savefig(opdir+"all_model_apparent_mags.png") + plt.close() + + + +if __name__ == "__main__": + + calc_path_priors() + + + diff --git a/papers/pathpriors/plot_marnoch_model.py b/papers/pathpriors/plot_marnoch_model.py index 54f98053..27cd8540 100644 --- a/papers/pathpriors/plot_marnoch_model.py +++ b/papers/pathpriors/plot_marnoch_model.py @@ -78,7 +78,7 @@ def main(): llstat = on.calculate_likelihood_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUobs,PUprior,plotfile=outfile) ksstat = on.calculate_ks_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,sumPUobs, - sumPUprior,plotfile=outfile,abc="(a)",tag="Marnoch: ") + sumPUprior,plotfile=outfile,abc="(a)",tag="Marnoch23: ") print("Best-fit stats of the Marnoch model are ll=",llstat," ks = ",ksstat) diff --git a/papers/pathpriors/simple_systematics.py b/papers/pathpriors/simple_systematics.py new file mode 100644 index 00000000..5de6f412 --- /dev/null +++ b/papers/pathpriors/simple_systematics.py @@ -0,0 +1,251 @@ +""" +This file fits the simple (naive) model to CRAFT ICDS observations. +It varies the simple model parameterisation, to determine systematic effects +""" + + +#standard Python imports +import os +import numpy as np +from matplotlib import pyplot as plt +from scipy.optimize import minimize +from scipy.stats import chi2 + +# imports from the "FRB" series +from zdm import optical as opt +from zdm import optical_params as op +from zdm import loading +from zdm import cosmology as cos +from zdm import parameters +from zdm import loading +from zdm import optical_numerics as on +from zdm import states + +# other FRB library imports +import astropath.priors as pathpriors + +import matplotlib + +defaultsize=14 +ds=4 +font = {'family' : 'Helvetica', + 'weight' : 'normal', + 'size' : defaultsize} +matplotlib.rc('font', **font) + + + + +def main(): + """ + Main routine + Loops over parameterisations, and plots best fits + """ + + ######### List of all ICS FRBs for which we can run PATH ####### + # hard-coded list of FRBs with PATH data in ice paper + frblist=opt.frblist + + # Initlisation of zDM grid + # Eventually, this should be part of the loop, i.e. host IDs should + # be re-fed into FRB surveys. However, it will be difficult to do this + # with very limited redshift estimates. That might require posterior + # estimates of redshift given the observed galaxies. Maybe. + state = states.load_state("HoffmannHalo25",scat=None,rep=None) + + # initialise cosmology + cos.set_cosmology(state) + cos.init_dist_measures() + + names=['CRAFT_ICS_892','CRAFT_ICS_1300','CRAFT_ICS_1632'] + ss,gs = loading.surveys_and_grids(survey_names=names,init_state=state) + + + NBinList = [6,11,21] + AbsminList = np.linspace(-26,-25,11) + AbsmaxList = AbsminList + 10. # increases max, but not in same way + + plt.figure() + plt.xlabel("Absolute magnitude, $M_r$") + plt.ylabel("$p(M_r)$") + + count = 0 + + opdir = "simple_systematics/" + if not os.path.exists(opdir): + os.mkdir(opdir) + + load = False + + colours = ["grey","orange","blue"] + markers = ['o','x','s'] + xlist = [] + llstats=[] + ksstats=[] + kvals=[] + for i,NModelBins in enumerate(NBinList): + llstats.append([]) + ksstats.append([]) + kvals.append([]) + for j,Absmin in enumerate(AbsminList): + Absmax = AbsmaxList[j] + fname1 = opdir + "bins_"+str(count)+".npy" + fname2 = opdir + "allx_"+str(count)+".npy" + + + print("Doing model with ",Absmin,Absmax,NModelBins) + + opstate = op.OpticalState() + # sets optical state to use simple linear interpolation + opstate.simple.AbsModelID = 1 + opstate.simple.AppModelID = 1 # k-correction + opstate.simple.Absmin = Absmin + opstate.simple.Absmax = Absmax + opstate.simple.NModelBins = NModelBins + + if load: + bins = np.load(fname1) + allx = np.load(fname2) + model = opt.simple_host_model(opstate) + else: + + #AbsMags = np.linspace(Ansmin,Absmax,NAbsBins) + allx,model = get_best_fit(ss,gs,frblist,opstate) + + bins = model.ModelBins + np.save(fname1,bins) + np.save(fname2,allx) + + + model.init_args(allx) + wrappers = on.make_wrappers(model,gs) + NFRB,AppMags,AppMagPriors,ObsMags,ObsPriors,ObsPosteriors,PUprior,PUobs,sumPUprior,sumPUobs,frbs,dms = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) + + llstat = on.calculate_likelihood_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUobs,PUprior) + ksstat = on.calculate_ks_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,sumPUobs, + sumPUprior) + + llstats[i].append(llstat) + ksstats[i].append(ksstat) + kvals[i].append(allx[0]) + x = allx[1:] + xlist.append(x) + if j==0: + plt.plot(bins,x/np.sum(x)*NModelBins,color=colours[i],label=str(NModelBins)+" bins",marker=markers[i]) + #plt.plot(bins,x/np.sum(x),marker="o",linestyle="",color=colours[i]) + else: + plt.plot(bins,x/np.sum(x)*NModelBins,color=colours[i],marker=markers[i]) + #plt.plot(bins,x/np.sum(x),marker="o",linestyle="",color=colours[i]) + + + + count += 1 + + plt.legend() + plt.tight_layout() + plt.savefig(opdir+"model_systematics.png") + plt.close() + + plt.figure() + print("kvals") + for i,NModelBins in enumerate(NBinList): + plt.plot(kvals[i],label="Nbins = "+str(NModelBins),linestyle="",marker='o') + print(i,NModelBins,np.mean(kvals[i]),np.std(kvals[i])) + plt.savefig(opdir+"kvals.png") + plt.close() + + plt.figure() + print("ks stats") + for i,NModelBins in enumerate(NBinList): + plt.plot(ksstats[i],label="Nbins = "+str(NModelBins),linestyle="",marker='o') + print(i,NModelBins,np.mean(ksstats[i]),np.std(ksstats[i])) + plt.savefig(opdir+"ksstats.png") + plt.close() + + plt.figure() + print("llstats") + for i,NModelBins in enumerate(NBinList): + plt.plot(llstats[i],label="Nbins = "+str(NModelBins),linestyle="",marker='o') + print(i,NModelBins,np.mean(llstats[i]),np.std(llstats[i])) + plt.savefig(opdir+"llstats.png") + plt.close() + + for j,Absmin in enumerate(AbsminList): + # three tests : 20 vs 10, 10 vs 5, 20 vs 5 + dl02 = -2. * (llstats[0][j]-llstats[2][j]) + dl01 = -2. * (llstats[0][j]-llstats[1][j]) + dl12 = -2. * (llstats[1][j]-llstats[2][j]) + + ddf02 = 15 + ddf01 = 5 + ddf12 = 10 + + p02 = 1.-chi2.cdf(dl02,ddf02) + p12 = 1.-chi2.cdf(dl12,ddf12) + p01 = 1.-chi2.cdf(dl01,ddf01) + print(j, "th offset: p-values for 5-10, 10-20, and 5-20 are ",p01,p12,p02, " with stats ",dl01,dl12,dl02) + +def get_best_fit(ss,gs,frblist,opstate): + """ + Fits simple model parameterised by: + + Args: + ss: list of survey objects + gs: list of grid objects + frblist: list of FRBs to process + opstate: optical state to be used in modelling + + Returns: + Best-fit parameters + + """ + + + ######## Determnine which statistic to use in optimisation ######## + # setting istat=0 means using a ks statistic to fit p(m_r) + # setting istat=1 means using a maximum likelihood estimator + istat=1 + # dok=True means use the k-correction + dok = True + # we are using the simple model + modelname = "simple" + # set to e.g. 0.9 to reject FRBs with lower posteriors when doing model comparisons + POxcut = None + + + # Case of simple host model + # Initialisation of model + # simple host model + + # sets up initial bounds on variables + if dok: + opstate.simple.AppModelID = 1 # k-correction + Nparams = opstate.simple.NModelBins+1 + opstate.simple.AppModelID = 1 # sets to include k-correction + opstate.simple.k = 0.5 # for some reason, this just doesn't make much difference to results + bounds = [(-25,25)]+[(0,1)]*(Nparams-1) + else: + Nparams = opstate.simple.NModelBins + # bins now give log-space values, hence -5,2 is range of 10^7 + if opstate.simple.AbsModelID == 3: + base=(-5,2) # log space + else: + base=(0,1) # linear space + bounds = [base]*(Nparams) + opstate.simple.AppModelID = 0 # no k-correction + + model = opt.simple_host_model(OpticalState = opstate) + + # retrieve default initial arguments in vector form + x0 = model.get_args() + # initialise aguments to minimisation function + args=[frblist,ss,gs,model,POxcut,istat] + + # set to false to just use hard-coded best fit parameters + minimise=True + result = minimize(on.function,x0 = x0,args=args,bounds = bounds) + x = result.x + return x,model + +main() + diff --git a/zdm/optical.py b/zdm/optical.py index 36fcc3e7..65bd7907 100644 --- a/zdm/optical.py +++ b/zdm/optical.py @@ -111,7 +111,6 @@ def process_rbands(self): """ Returns parameters of the host magnitude distribution as a function of redshift """ - #FRBlist=["FRB20180301A FRB20180916B FRB20190520B FRB20201124A FRB20210410D FRB20121102A FRB20180924B FRB20181112A FRB20190102C FRB20190608B FRB20190611B FRB20190711A FRB20190714A FRB20191001A FRB20200430A FRB20200906A FRB20210117A FRB20210320C FRB20210807D FRB20211127I FRB20211203C FRB20211212A FRB20220105A] table = self.table colnames = table.colnames @@ -580,7 +579,6 @@ def init_abs_bins(self): Absmax = self.opstate.Absmax NAbsBins = self.opstate.NAbsBins - self.Absmin = Absmin self.Absmax = Absmax self.NAbsBins = NAbsBins @@ -703,6 +701,7 @@ def get_pmr_gz(self,mrbins,z): # linear interpolation # note that dMr = dmr, so we just map probability densities + kmag2s = (Mrvals - self.Absmin)/self.dMag imag1s = np.floor(kmag2s).astype('int') kmag2s -= imag1s @@ -1460,13 +1459,15 @@ def matchFRB(TNSname,survey): # this defines the ICS FRBs for which we have PATH info +# notes: FRB20230731A and 'FRB20230718A' are too reddened, so are removed +# still aiming to follow up frb20240208A and frb20240318A frblist=['FRB20180924B','FRB20181112A','FRB20190102C','FRB20190608B', 'FRB20190611B','FRB20190711A','FRB20190714A','FRB20191001A', 'FRB20191228A','FRB20200430A','FRB20200906A','FRB20210117A', 'FRB20210320C','FRB20210807D','FRB20210912A','FRB20211127I','FRB20211203C', 'FRB20211212A','FRB20220105A','FRB20220501C', 'FRB20220610A','FRB20220725A','FRB20220918A', - 'FRB20221106A','FRB20230526A','FRB20230708A', + 'FRB20221106A','FRB20230526A','FRB20230708A', 'FRB20230731A','FRB20230902A','FRB20231226A','FRB20240201A', 'FRB20240210A','FRB20240304A','FRB20240310A'] diff --git a/zdm/optical_params.py b/zdm/optical_params.py index 74b02e4d..31151b34 100644 --- a/zdm/optical_params.py +++ b/zdm/optical_params.py @@ -24,13 +24,13 @@ class SimpleParams(data_class.myDataClass): """ # None of the fields should start with an X Absmin: float = field( - default=-30, + default=-25, metadata={'help': "Minimum host absolute magnitude", 'unit': 'M_r^{min}', 'Notation': '', }) Absmax: float = field( - default=-10., + default=-15., metadata={'help': "Maximum host absolute magnitude", 'unit': 'M_r^{max}', 'Notation': '', From a5f211b2d882f2e50bad1605af010a3cb1154b6e Mon Sep 17 00:00:00 2001 From: Clancy James Date: Tue, 3 Mar 2026 08:41:55 +0800 Subject: [PATCH 22/35] tidied up scripts associated with PATH. Likely need more tidying --- zdm/optical_numerics.py | 1 + zdm/scripts/Path/estimate_path_priors.py | 24 ++++++++++-------- zdm/scripts/Path/optimise_host_priors.py | 21 ++++++++++------ zdm/scripts/Path/plot_host_models.py | 32 ++++++++++++++++++------ 4 files changed, 51 insertions(+), 27 deletions(-) diff --git a/zdm/optical_numerics.py b/zdm/optical_numerics.py index 3de9a8f7..e6664062 100644 --- a/zdm/optical_numerics.py +++ b/zdm/optical_numerics.py @@ -543,6 +543,7 @@ def run_path(name,P_U=0.1,usemodel = False, sort=False): evaluates PATH on an FRB Args: + name [string]: TNS name of FRB P_U [float]: unseen prior usemodel [bool]: if True, use user-defined P_O|x model sort [bool]: if True, sort candidates by posterior diff --git a/zdm/scripts/Path/estimate_path_priors.py b/zdm/scripts/Path/estimate_path_priors.py index 133742ac..36cf0d84 100644 --- a/zdm/scripts/Path/estimate_path_priors.py +++ b/zdm/scripts/Path/estimate_path_priors.py @@ -13,6 +13,7 @@ # imports from the "FRB" series from zdm import optical as opt +from zdm import optical_numerics as on from zdm import loading from zdm import cosmology as cos from zdm import parameters @@ -34,21 +35,18 @@ def calc_path_priors(): state = parameters.State() cos.set_cosmology(state) cos.init_dist_measures() - model = opt.host_model() + model = opt.marnoch_model() name='CRAFT_ICS_1300' ss,gs = loading.surveys_and_grids(survey_names=[name]) g = gs[0] s = ss[0] # must be done once for any fixed zvals - model.init_zmapping(g.zvals) + wrapper = opt.model_wrapper(model,g.zvals) # do this only for a particular FRB # it gives a prior on apparent magnitude and pz #AppMagPriors,pz = model.get_posterior(g,DMlist) - # do this once per "model" objects - pathpriors.USR_raw_prior_Oi = model.path_raw_prior_Oi - allmags = None allPOx = None @@ -68,12 +66,16 @@ def calc_path_priors(): DMEG = s.DMEGs[imatch] + # + # original calculation - P_O1,P_Ox1,P_Ux1,mags1 = opt.run_path(frb,model,usemodel=False,PU=0.1) + P_O1,P_Ox1,P_Ux1,mags1,ptbl = on.run_path(frb,usemodel=False,P_U=0.1) + + # initialises wrapper to give p(mr|DMEG) for p(z|DM) grid predictions + wrapper.init_path_raw_prior_Oi(DMEG,g) + PU = wrapper.estimate_unseen_prior() - model.init_path_raw_prior_Oi(DMEG,g) - PU = model.estimate_unseen_prior(mag_limit=26) # might not be correct - P_O2,P_Ox2,P_Ux2,mags2 = opt.run_path(frb,model,usemodel=True,PU = PU) + P_O2,P_Ox2,P_Ux2,mags2,ptbl = on.run_path(frb,usemodel=True,P_U = PU) if False: # compares outcomes @@ -95,8 +97,8 @@ def calc_path_priors(): allmags = np.append(allmags,mags2) allPOx = np.append(allPOx,P_Ox2) - Nbins = int(model.Appmax - model.Appmin)+1 - bins = np.linspace(model.Appmin,model.Appmax,Nbins) + Nbins = int(wrapper.Appmax - wrapper.Appmin)+1 + bins = np.linspace(wrapper.Appmin,wrapper.Appmax,Nbins) plt.figure() plt.hist(allmags,weights = allPOx, bins = bins,label="Posterior") plt.legend() diff --git a/zdm/scripts/Path/optimise_host_priors.py b/zdm/scripts/Path/optimise_host_priors.py index 66b1a67d..a1da24fb 100644 --- a/zdm/scripts/Path/optimise_host_priors.py +++ b/zdm/scripts/Path/optimise_host_priors.py @@ -128,18 +128,22 @@ def main(): # "function" is the function that performs the comparison of # predictions to outcomes. It's where all the magic happens - result = minimize(on.function,x0 = x0,args=args,bounds = bounds) - print("Best fit result is ",result.x) - x = result.x - # saves result - np.save(opdir+"/best_fit_params.npy",x) + minimise=True + if minimise: + result = minimize(on.function,x0 = x0,args=args,bounds = bounds) + print("Best fit result is ",result.x) + x = result.x + # saves result + np.save(opdir+"/best_fit_params.npy",x) + else: + x = np.load(opdir+"/best_fit_params.npy") # initialises arguments model.init_args(x) outfile = opdir+"best_fit_apparent_magnitudes.png" wrappers = on.make_wrappers(model,gs) - NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUprior,PUobs,sumPUprior,sumPUobs = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) + NFRB,AppMags,AppMagPriors,ObsMags,ObsPriors,ObsPosteriors,PUprior,PUobs,sumPUprior,sumPUobs,frbs,dms = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) # calculates a maximum-likelihood statistic stat = on.calculate_likelihood_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUobs,PUprior,plotfile=outfile) @@ -150,8 +154,9 @@ def main(): # calculates the original PATH result outfile = opdir+"original_fit_apparent_magnitudes.png" - NFRB2,AppMags2,AppMagPriors2,ObsMags2,ObsPosteriors2,PUprior2,PUobs2,sumPUprior2,sumPUobs2 = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False,usemodel=False) - stat = on.calculate_ks_statistic(NFRB,AppMags2,AppMagPriors2,ObsMags2,ObsPosteriors2,sumPUobs2,sumPUprior2,plotfile=outfile) + NFRB2,AppMags2,AppMagPriors2,ObsMags2,ObsPriors2,ObsPosteriors2,PUprior2,PUobs2,sumPUprior2,sumPUobs2,frbs,dms = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False,usemodel=False) + # currently, calculating KS statistics does not work/make sense for original path. need to re-think this + #stat = on.calculate_ks_statistic(NFRB,AppMags,AppMagPriors2,ObsMags2,ObsPosteriors2,sumPUobs2,sumPUprior2,plotfile=outfile) # flattens lists of lists ObsPosteriors = [x for xs in ObsPosteriors for x in xs] diff --git a/zdm/scripts/Path/plot_host_models.py b/zdm/scripts/Path/plot_host_models.py index ba52c462..b40d7a89 100644 --- a/zdm/scripts/Path/plot_host_models.py +++ b/zdm/scripts/Path/plot_host_models.py @@ -7,6 +7,10 @@ We then evaluate P(O|x) for CRAFT FRBs in the CRAFT 1300 MHz survey +It's not the simplest script, but it should show how to do a whole bunch of stuff + +NOTE: this does NOT use the best-fit distributions form the recent paper. + """ #standard Python imports @@ -26,6 +30,16 @@ import astropath.priors as pathpriors +import matplotlib + +defaultsize=14 +ds=4 +font = {'family' : 'Helvetica', + 'weight' : 'normal', + 'size' : defaultsize} +matplotlib.rc('font', **font) + + def calc_path_priors(): """ Loops over all ICS FRBs @@ -95,8 +109,8 @@ def calc_path_priors(): plt.figure() plt.xlabel("$m_r$") - plt.ylabel("p(m_r | z=0.5)$") - + plt.ylabel("$P(m_r | z=0.5)$") + styles=["-","--",":","-.","-","--",":","-."] fsfrs = np.linspace(-2,3,6) # extrapolates to weird values z=0.5 mrbins = np.linspace(0,40,401) @@ -104,8 +118,10 @@ def calc_path_priors(): for i,fsfr in enumerate(fsfrs): model2.init_args(fsfr) pmr = model2.get_pmr_gz(mrbins,z) - plt.plot(rbc,pmr,label="$f_{\\rm sfr} = $"+str(fsfr)[:5]) + pmr /= np.sum(pmr)*(rbc[1]-rbc[0]) + plt.plot(rbc,pmr,label="$f_{\\rm sfr} = $"+str(fsfr)[:5],linestyle=styles[i]) plt.xlim(15,30) + plt.ylim(0.,0.6) plt.legend() plt.tight_layout() plt.savefig(opdir+"loudas_fsfr_interpolation.png") @@ -247,7 +263,7 @@ def calc_path_priors(): DMEG = s.DMEGs[imatch] # original calculation - P_O1,P_Ox1,P_Ux1,mags1,ptbl = on.run_path(frb,usemodel=False,PU=0.1) + P_O1,P_Ox1,P_Ux1,mags1,ptbl = on.run_path(frb,usemodel=False,P_U=0.1) # record this info if maglist[0] is None: @@ -262,7 +278,7 @@ def calc_path_priors(): wrapper1.init_path_raw_prior_Oi(DMEG,g) PU2 = wrapper1.estimate_unseen_prior() # might not be correct pathpriors.USR_raw_prior_Oi = wrapper1.path_raw_prior_Oi - P_O2,P_Ox2,P_Ux2,mags2,ptbl = on.run_path(frb,usemodel=True,PU = PU2) + P_O2,P_Ox2,P_Ux2,mags2,ptbl = on.run_path(frb,usemodel=True,P_U = PU2) for imag,mag in enumerate(mags2): @@ -288,7 +304,7 @@ def calc_path_priors(): wrapper2.init_path_raw_prior_Oi(DMEG,g) PU3 = wrapper2.estimate_unseen_prior() # might not be correct pathpriors.USR_raw_prior_Oi = wrapper2.path_raw_prior_Oi - P_O3,P_Ox3,P_Ux3,mags3,ptbl = on.run_path(frb,usemodel=True,PU = PU3) + P_O3,P_Ox3,P_Ux3,mags3,ptbl = on.run_path(frb,usemodel=True,P_U = PU3) # record this info if maglist[2] is None: @@ -306,7 +322,7 @@ def calc_path_priors(): wrapper2.init_path_raw_prior_Oi(DMEG,g) PU4 = wrapper2.estimate_unseen_prior() # might not be correct limit pathpriors.USR_raw_prior_Oi = wrapper2.path_raw_prior_Oi - P_O4,P_Ox4,P_Ux4,mags4,ptbl = on.run_path(frb,usemodel=True,PU = PU4) + P_O4,P_Ox4,P_Ux4,mags4,ptbl = on.run_path(frb,usemodel=True,P_U = PU4) # record this info if maglist[3] is None: @@ -321,7 +337,7 @@ def calc_path_priors(): wrapper3.init_path_raw_prior_Oi(DMEG,g) PU5 = wrapper3.estimate_unseen_prior() # might not be correct limit pathpriors.USR_raw_prior_Oi = wrapper3.path_raw_prior_Oi - P_O5,P_Ox5,P_Ux5,mags5,ptbl = on.run_path(frb,usemodel=True,PU = PU5) + P_O5,P_Ox5,P_Ux5,mags5,ptbl = on.run_path(frb,usemodel=True,P_U = PU5) # record this info if maglist[4] is None: From 9dad2bc2560a0fd8457386c33e5ef5881c140d40 Mon Sep 17 00:00:00 2001 From: Clancy James Date: Mon, 16 Mar 2026 15:39:32 +0800 Subject: [PATCH 23/35] Updated for last paper internal review, and SKA deferral calculations --- papers/SKA_science/Deferral/calc_fom.py | 161 ++++++++++++++++++ papers/SKA_science/Deferral/option_5.2.txt | 1 + papers/SKA_science/Deferral/option_5.txt | 1 + papers/SKA_science/Deferral/option_7.txt | 1 + papers/pathpriors/compare_posteriors.py | 112 ++++++++++++- papers/pathpriors/fit_loudas_model.py | 3 +- papers/pathpriors/fit_simple_model.py | 4 +- papers/pathpriors/get_pu_dist.py | 167 +++++++++++++++++++ papers/pathpriors/pO_g_mr/fit_pogmr.py | 2 +- papers/pathpriors/plot_colors.py | 70 ++++++++ papers/pathpriors/plot_craft_optical_data.py | 4 +- papers/pathpriors/plot_host_models.py | 6 +- zdm/optical.py | 8 +- zdm/optical_numerics.py | 76 ++++++--- 14 files changed, 575 insertions(+), 41 deletions(-) create mode 100644 papers/SKA_science/Deferral/calc_fom.py create mode 100644 papers/SKA_science/Deferral/option_5.2.txt create mode 100644 papers/SKA_science/Deferral/option_5.txt create mode 100644 papers/SKA_science/Deferral/option_7.txt create mode 100644 papers/pathpriors/get_pu_dist.py create mode 100644 papers/pathpriors/plot_colors.py diff --git a/papers/SKA_science/Deferral/calc_fom.py b/papers/SKA_science/Deferral/calc_fom.py new file mode 100644 index 00000000..7e611df8 --- /dev/null +++ b/papers/SKA_science/Deferral/calc_fom.py @@ -0,0 +1,161 @@ +import numpy as np +import pandas as pd +from matplotlib import pyplot as plt + + +def main(iTEL,fbeams,tag): + + print("\n\n\n\n\nGENERATING RESULTS FOR ",tag,"\n\n") + if iTEL==0: + options,labels = read_options() + + ####### gets original data ####### + all_station_datas = read_keane() + all_station_data = all_station_datas[iTEL] + + all_stations = all_station_data[0] + radii = all_station_data[1] + sens = all_station_data[2] + FOV = all_station_data[3] + + + # gets previous best radius (same for both configs) + prev_bests = ["C224","SKA041","SKA041"] # For AA4 + + sbest = prev_bests[iTEL] + ibest = np.where(all_stations == sbest)[0] + rmax = radii[ibest].values + print("Orig max radius is ",rmax," with ",ibest+1,"stations") + + + plt.figure() + + plt.plot([rmax,rmax],[0,512],color="black",linestyle=":") + plt.text(rmax*1.1,350,"Pre-deferral\noptimum",rotation=90,fontsize=12) + l1,=plt.plot(radii,np.arange(radii.size)+1,label="original AA4",color="black") + + # this step limits the size of the FOV to the HPBW + eff_rad = np.copy(radii.values) + toolow = np.where(eff_rad < rmax*fbeams**0.5)[0] + eff_rad[toolow] = rmax*fbeams**0.5 + + FOM = eff_rad**-2 * (np.arange(radii.size)+1)**1.5 + old_max = FOM[ibest] + ax1 = plt.gca() + ax2 = ax1.twinx() + + + if iTEL > 0: + imax = np.argmax(FOM[1:])+1 + rmax = radii[imax] + newmax = fbeams * FOM[imax] + print("We find a new peak maximum at r = ",rmax," using ",imax+1," antennas", "reduction of ",newmax/old_max) + l2,=ax2.plot(radii,FOM/FOM[ibest]*fbeams,linestyle="--",label="Relative (deferral)") + + + + + #ax2.plot(radii,FOM,color="black",linestyle="--") + + # loop over all options + # plots graph of radius vs antenna number for each options + if iTEL==0: + for i,option in enumerate(options): + matches = identify_present(option,all_stations) + nstations = len(matches) + stations = np.arange(nstations)+1 + plot_r = np.array(radii[matches].values) + ax1.plot(plot_r,stations,label=labels[i]) + + FOM = stations**1.5 / plot_r**2 + FOM_max = np.max(FOM[1:]) + Nmax = np.argmax(FOM[1:])+1 + rmax = plot_r[Nmax] + new_max = FOM_max * fbeams + + ax2.plot(plot_r[1:],fbeams*FOM[1:]/old_max,color=ax1.lines[-1].get_color(),linestyle="--") + + Nless = np.where(plot_r <= rmax)[0][-1] + print("Options ",i," Number of stations included is ",Nless+1) + print("New FOM is ",new_max/old_max," of old efficiency at rmax = ",rmax, Nmax+1) + plt.sca(ax2) + plt.ylabel("Fraction of AA4 FOM") + plt.sca(ax1) + plt.ylim(0,512) + plt.xscale('log') + + if iTEL==0: + plt.ylabel("Number of stations") + plt.legend() + else: + plt.ylabel("Number of antennas") + plt.legend(handles=[l1,l2],labels=["Array density","Relative FOM (deferral)"]) + plt.xlabel("Radius [km]") + plt.tight_layout() + plt.savefig(tag+"stations_vs_radius.png") + plt.close() + + +def get_optimum(): + """ + Gets optimum trade-off + """ + +def identify_present(subset,full_list): + """ + identifies which subset is present, i.e., if all antennas are actually there + """ + + ns = len(subset) + matches = np.zeros([ns],dtype='int') + + for i,station in enumerate(subset): + + match = np.where(full_list == station)[0] + if len(match)==0: + print("could not find station ",station) + continue + matches[i] = match + + sort_matches = np.sort(matches) + + return sort_matches + +def read_keane(): + """ + reads Evan's info + """ + + files = ["../inputs/LowAA4_ID_radius_AonT_FoVdeg2","../inputs/Band1AA4_ID_radius_AonT_FoVdeg2","../inputs/Band2AA4_ID_radius_AonT_FoVdeg2"] + #files = ["../inputs/LowAAstar_ID_radius_AonT_FoVdeg2","../inputs/Band1AAstar_ID_radius_AonT_FoVdeg2","../inputs/Band2AAstar_ID_radius_AonT_FoVdeg2"] + + + #data = np.loadtxt(f,dtype='string') + datas=[] + for f in files: + data = pd.read_csv(f, sep='\s+', header=None) + datas.append(data) + return datas + +def read_options(): + """ + reads in options + """ + + options = ["option_5.txt","option_5.2.txt","option_7.txt"] + option_labels = ["option 5","option 5.2","option 7"] + stations=[] + for i,option in enumerate(options): + with open(option, 'r') as f: + for line in f: + slist = line.split(',') + slist[-1] = slist[-1][:-1] # to avoid last \n + stations.append(slist) + return stations,option_labels + +fbeams = [50./250,200./1125,200./1125] + +labels=["Low","Mid_band1","mid_band2"] +for iTEL in np.arange(3): + main(iTEL,fbeams[iTEL],labels[iTEL]) + diff --git a/papers/SKA_science/Deferral/option_5.2.txt b/papers/SKA_science/Deferral/option_5.2.txt new file mode 100644 index 00000000..82c14add --- /dev/null +++ b/papers/SKA_science/Deferral/option_5.2.txt @@ -0,0 +1 @@ +C1,C10,C100,C103,C108,C11,C111,C112,C113,C117,C12,C120,C121,C123,C124,C125,C126,C128,C13,C130,C132,C138,C139,C14,C141,C142,C143,C144,C145,C147,C15,C153,C156,C158,C16,C161,C162,C163,C164,C167,C168,C17,C170,C171,C172,C173,C175,C176,C177,C179,C18,C181,C184,C187,C19,C190,C191,C193,C194,C197,C198,C199,C2,C20,C200,C201,C203,C204,C206,C208,C212,C214,C217,C219,C22,C23,C24,C25,C26,C27,C28,C29,C3,C30,C31,C32,C33,C34,C35,C36,C37,C38,C39,C4,C41,C42,C43,C44,C45,C46,C47,C48,C49,C5,C50,C51,C52,C53,C54,C55,C56,C57,C58,C59,C6,C60,C61,C62,C63,C64,C65,C66,C67,C68,C69,C7,C70,C71,C72,C73,C74,C75,C76,C77,C78,C79,C8,C81,C82,C83,C84,C86,C87,C88,C89,C9,C91,C98,C99,E1-3,E1-4,E10-1,E10-2,E10-3,E10-4,E13-1,E13-2,E13-3,E13-4,E15-1,E15-2,E15-3,E15-4,E16-1,E16-2,E16-3,E16-4,E2-1,E2-2,E2-3,E3-1,E3-3,E3-4,E4-1,E4-2,E4-3,E8-1,E8-2,E8-3,E8-4,E9-1,E9-2,E9-3,E9-4,N1-1,N1-6,N10-1,N10-2,N10-3,N10-4,N13-1,N13-2,N13-3,N13-4,N15-1,N15-2,N15-3,N15-4,N16-1,N16-2,N16-3,N16-4,N2-1,N2-2,N2-3,N3-1,N3-2,N3-3,N4-1,N4-2,N4-3,N8-1,N8-2,N8-3,N8-4,N9-1,N9-2,N9-3,N9-4,S1-1,S1-2,S10-1,S10-2,S10-3,S10-4,S10-5,S10-6,S13-1,S13-2,S13-3,S13-4,S15-1,S15-2,S15-3,S15-4,S16-1,S16-2,S16-3,S16-4,S2-4,S2-5,S2-6,S3-2,S3-4,S4-2,S4-3,S4-6,S8-1,S8-2,S8-3,S8-4,S8-5,S8-6,S9-1,S9-2,S9-3,S9-4 diff --git a/papers/SKA_science/Deferral/option_5.txt b/papers/SKA_science/Deferral/option_5.txt new file mode 100644 index 00000000..252374bb --- /dev/null +++ b/papers/SKA_science/Deferral/option_5.txt @@ -0,0 +1 @@ +C1,C10,C100,C103,C108,C11,C111,C112,C113,C117,C12,C120,C121,C123,C124,C125,C126,C128,C13,C130,C132,C138,C139,C14,C141,C142,C143,C144,C145,C147,C15,C153,C156,C158,C16,C161,C162,C163,C164,C167,C168,C17,C170,C171,C172,C173,C175,C176,C177,C179,C18,C181,C184,C187,C19,C190,C191,C193,C194,C197,C198,C199,C2,C20,C200,C201,C203,C204,C206,C208,C212,C214,C216,C219,C22,C23,C24,C25,C26,C27,C28,C29,C3,C30,C31,C32,C33,C34,C35,C36,C37,C38,C39,C4,C41,C42,C43,C44,C45,C46,C47,C48,C49,C5,C50,C51,C52,C53,C54,C55,C56,C57,C58,C59,C6,C60,C61,C62,C63,C64,C65,C66,C67,C68,C69,C7,C70,C71,C72,C73,C74,C75,C76,C77,C78,C79,C8,C81,C82,C83,C84,C86,C87,C88,C89,C9,C91,C98,C99,E1-1,E1-2,E10-1,E10-2,E10-3,E10-4,E13-1,E13-2,E13-3,E13-4,E15-1,E15-2,E15-3,E15-4,E16-1,E16-2,E16-3,E16-4,E2-1,E2-2,E2-3,E3-1,E3-2,E3-3,E4-1,E4-2,E4-3,E8-1,E8-2,E8-3,E8-4,E9-1,E9-2,E9-3,E9-4,N1-1,N1-2,N10-1,N10-2,N10-3,N10-4,N13-1,N13-2,N13-3,N13-4,N15-1,N15-2,N15-3,N15-4,N16-1,N16-2,N16-3,N16-4,N2-1,N2-2,N2-3,N3-1,N3-2,N3-3,N4-1,N4-2,N4-3,N8-1,N8-2,N8-3,N8-4,N9-1,N9-2,N9-3,N9-4,S1-1,S1-2,S10-1,S10-2,S10-3,S10-4,S10-5,S10-6,S13-1,S13-2,S13-3,S13-4,S15-1,S15-2,S15-3,S15-4,S16-1,S16-2,S16-3,S16-4,S2-1,S2-2,S2-3,S3-1,S3-2,S4-1,S4-2,S4-3,S8-1,S8-2,S8-3,S8-4,S8-5,S8-6,S9-1,S9-2,S9-3,S9-4 diff --git a/papers/SKA_science/Deferral/option_7.txt b/papers/SKA_science/Deferral/option_7.txt new file mode 100644 index 00000000..fb96e993 --- /dev/null +++ b/papers/SKA_science/Deferral/option_7.txt @@ -0,0 +1 @@ +C1,C10,C100,C103,C108,C11,C111,C112,C113,C117,C12,C120,C121,C123,C124,C125,C126,C128,C13,C130,C132,C138,C139,C14,C141,C142,C143,C144,C145,C147,C15,C153,C156,C158,C16,C161,C162,C163,C164,C167,C168,C17,C170,C171,C172,C173,C175,C176,C177,C179,C18,C181,C184,C187,C19,C190,C191,C193,C194,C197,C198,C199,C2,C20,C200,C201,C203,C204,C205,C206,C208,C21,C212,C214,C217,C219,C22,N4-1,N4-3,C23,C24,C25,C26,C28,C29,C3,C31,C33,C35,C36,C37,C38,C39,C4,C41,C42,C43,C45,C46,C47,C48,C49,C5,C50,C52,C53,C54,C55,C56,C57,C58,C59,C6,C60,C61,C62,C63,C65,C66,C67,C68,C69,C7,C70,C71,C72,C73,C74,C75,C76,C77,C79,C8,C81,C82,C83,C84,C86,C87,C88,C9,C91,C98,C99,E1-3,E1-4,S4-6,E10-1,E10-2,E10-3,E10-4,E13-1,E13-2,E13-3,E13-4,E15-1,E15-2,E15-3,E15-4,E16-1,E16-2,E16-3,E16-4,E2-2,E2-3,E2-4,E3-1,E3-3,E3-4,E4-1,E4-2,E4-3,E8-1,E8-2,E8-3,E8-4,E9-1,E9-2,E9-3,E9-4,N1-1,N1-6,N10-1,N10-2,N10-3,N10-4,N13-1,N13-2,N13-3,N13-4,N15-1,N15-2,N15-3,N15-4,N16-1,N16-2,N16-3,N16-4,N2-1,N2-2,N2-3,N3-1,N3-2,N3-3,N4-2,N8-1,N8-2,N8-3,N8-4,N9-1,N9-2,N9-3,N9-4,S1-1,S1-2,S3-2,C222,C223,C94,C136,S10-1,S10-2,S10-3,S10-4,S10-5,S10-6,S13-1,S13-2,S13-3,S13-4,S15-1,S15-2,S15-3,S15-4,S16-1,S16-2,S16-3,S16-4,S2-4,S2-5,S2-6,S3-4,S4-2,S4-3,C64,C89,C78,S8-1,S8-2,S8-3,S8-4,S8-5,S8-6,S9-1,S9-2,S9-3,S9-4 diff --git a/papers/pathpriors/compare_posteriors.py b/papers/pathpriors/compare_posteriors.py index 982cef92..9fc7c8b7 100644 --- a/papers/pathpriors/compare_posteriors.py +++ b/papers/pathpriors/compare_posteriors.py @@ -64,18 +64,24 @@ def main(): ss,gs = loading.surveys_and_grids(survey_names=names,init_state=state) - # initialise figure + # initialise figure. ax1 is new vs old. ax2 is new-old vs old plt.figure() + ax1=plt.gca() plt.xlabel("$P(O_i| \\mathbf{x})$ [original; $P(U)=0.1$]") plt.ylabel("$P(O_i| \\mathbf{x},N_O)$ [this work]") + plt.figure() + ax2=plt.gca() + plt.xlabel("$P(O_i| \\mathbf{x})$ [original; $P(U)=0.1$]") + plt.ylabel("$\Delta P(O_i| \\mathbf{x},N_O)$") + ##### Begins by calculating the original PATH result ##### # calculates the original PATH result wrappers = None NFRB2,AppMags2,AppMagPriors2,ObsMags2,ObsPrior2,ObsPosteriors2,PUprior2,PUobs2,sumPUprior2,sumPUobs2,frbs,dms = \ on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False,usemodel=False,P_U=0.1) - fObsPosteriors2 = on.flatten(ObsPosteriors2) + fObsPosteriors2 = np.array(on.flatten(ObsPosteriors2)) with open("posteriors/orig.txt",'w') as f: @@ -86,6 +92,18 @@ def main(): f.write("\n") + #### creates some lists to later pass to make_cumulative_plots #### + NFRBlist = [] + AppMagslist = [] + AppMagPriorslist = [] + ObsMagslist = [] + ObsPosteriorslist = [] + PUpriorlist = [] + PUobslist = [] + sumPUpriorlist = [] + sumPUobslist = [] + + ####### Model 1: Marnoch ######## # model 1: Marnoch @@ -94,8 +112,11 @@ def main(): wrappers = on.make_wrappers(model,gs) NFRB1,AppMags1,AppMagPriors1,ObsMags1,ObsPrior1,ObsPosteriors1,PUprior1,PUobs1,sumPUprior1,sumPUobs1,frbs,dms = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) - fObsPosteriors1 = on.flatten(ObsPosteriors1) + fObsPosteriors1 = np.array(on.flatten(ObsPosteriors1)) + plt.sca(ax1) plt.scatter(fObsPosteriors2,fObsPosteriors1,label="Marnoch23",marker='s') + plt.sca(ax2) + plt.scatter(fObsPosteriors2,fObsPosteriors1-fObsPosteriors2,label="Marnoch23",marker='s') with open("posteriors/marnoch.txt",'w') as f: @@ -113,8 +134,11 @@ def main(): wrappers = on.make_wrappers(model,gs) NFRB3,AppMags3,AppMagPriors3,ObsMags3,ObsPrior3,ObsPosteriors3,PUprior3,PUobs3,sumPUprior3,sumPUobs3,frbs,dms = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) - fObsPosteriors3 = on.flatten(ObsPosteriors3) + fObsPosteriors3 = np.array(on.flatten(ObsPosteriors3)) + plt.sca(ax1) plt.scatter(fObsPosteriors2,fObsPosteriors3,label="Loudas25",marker='x') + plt.sca(ax2) + plt.scatter(fObsPosteriors2,fObsPosteriors3-fObsPosteriors2,label="Loudas25",marker='x') with open("posteriors/loudas.txt",'w') as f: for i,frb in enumerate(frbs): @@ -124,6 +148,7 @@ def main(): f.write("\n") + ####### Model 3: Simple ######## # Case of simple host model @@ -147,9 +172,14 @@ def main(): wrappers = on.make_wrappers(model,gs) NFRB4,AppMags4,AppMagPriors4,ObsMags4,ObsPrior4,ObsPosteriors4,PUprior4,PUobs4,sumPUprior4,sumPUobs4,frbs,dms = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) - fObsPosteriors4 = on.flatten(ObsPosteriors4) + fObsPosteriors4 = np.array(on.flatten(ObsPosteriors4)) + plt.sca(ax1) plt.scatter(fObsPosteriors2,fObsPosteriors4,label="Naive",marker='o',s=20) + plt.sca(ax2) + plt.scatter(fObsPosteriors2,fObsPosteriors4-fObsPosteriors2,label="Naive",marker='o',s=20) + # format and save ax1 + plt.sca(ax1) plt.legend(loc="lower right") plt.xlim(0,1) plt.ylim(0,1) @@ -158,6 +188,23 @@ def main(): plt.savefig("posteriors/pox_comparison.png") plt.close() + # format and save ax2 + plt.sca(ax2) + plt.legend(loc="upper left") + plt.xlim(0,1) + plt.ylim(-0.2,0.2) + plt.plot([0,1],[0,0],color="black",linestyle=":") + plt.text(0.4,0.01,"no change") + plt.plot([0.8,1],[0.2,0],color="black",linestyle=":") + plt.text(0.85,0.08,"$P(O_i| \\mathbf{x})=1$",rotation=-60) + + plt.plot([0,0.2],[0,-0.2],color="black",linestyle=":") + plt.text(0.03,-0.18,"$P(O_i| \\mathbf{x})=0$",rotation=-60) + + plt.tight_layout() + plt.savefig("posteriors/delta_pox_comparison.png") + plt.close() + with open("posteriors/naive.txt",'w') as f: for i,frb in enumerate(frbs): f.write(str(i)+" "+frb+" "+str(dms[i])[0:5]+" "+str(PUprior4[i])[0:4]+"\n") @@ -197,4 +244,59 @@ def main(): string2 += f"{ObsPrior3[i][j]:.3f} & {ObsPosteriors3[i][j]:.3f} & " string2 += f"{ObsPrior4[i][j]:.3f} & {ObsPosteriors4[i][j]:.3f} \\\\ " print(string2) + + + + ######## Makes cumulative distribution KS-style plots + + # loads various marnoch models + model = opt.loudas_model() + xbest = np.load("loudas_output/best_fit_params.npy") + for f_sfr in [0,1,xbest]: + x=[f_sfr] + model.init_args(x) + wrappers = on.make_wrappers(model,gs) + NFRB,AppMags,AppMagPriors,ObsMags,ObsPriors,ObsPosteriors,PUprior,PUobs,sumPUprior,sumPUobs,frbs,dms = on.calc_path_priors(frblist,ss,gs,wrappers,verbose=False) + + NFRBlist.append(NFRB) + AppMagslist.append(AppMags) + AppMagPriorslist.append(AppMagPriors) + ObsMagslist.append(ObsMags) + ObsPosteriorslist.append(ObsPosteriors) + PUpriorlist.append(PUprior) + PUobslist.append(PUobs) + sumPUpriorlist.append(sumPUprior) + sumPUobslist.append(sumPUobs) + + # loads naive model + NFRBlist.append(NFRB4) + AppMagslist.append(AppMags4) + AppMagPriorslist.append(AppMagPriors4) + ObsMagslist.append(ObsMags4) + ObsPosteriorslist.append(ObsPosteriors4) + PUpriorlist.append(PUprior4) + PUobslist.append(PUobs4) + sumPUpriorlist.append(sumPUprior4) + sumPUobslist.append(sumPUobs4) + + + # loads Marnoch model + NFRBlist.append(NFRB1) + AppMagslist.append(AppMags1) + AppMagPriorslist.append(AppMagPriors1) + ObsMagslist.append(ObsMags1) + ObsPosteriorslist.append(ObsPosteriors1) + PUpriorlist.append(PUprior1) + PUobslist.append(PUobs1) + sumPUpriorlist.append(sumPUprior1) + sumPUobslist.append(sumPUobs1) + + + plotlabels=["Loudas25: $f_{\\rm sfr} = 0$", " $f_{\\rm sfr} = 1$", + " $f_{\\rm sfr} = 3$","Naive","Marnoch23"] + plotfile="Plots/all_cumulative.png" + NMODELS=5 + on.make_cumulative_plots(NMODELS,NFRBlist,AppMagslist,AppMagPriorslist,ObsMagslist,ObsPosteriorslist, + PUobslist,PUpriorlist,plotfile,plotlabels,POxcut=None,onlyobs=0,addpriorlabel=False) + main() diff --git a/papers/pathpriors/fit_loudas_model.py b/papers/pathpriors/fit_loudas_model.py index f17ff22d..d498e11c 100644 --- a/papers/pathpriors/fit_loudas_model.py +++ b/papers/pathpriors/fit_loudas_model.py @@ -83,6 +83,7 @@ def main(): minimise=True if minimise: result = minimize(on.function,x0 = x0,args=args,bounds = bounds) + print("Best fit result is f_sfr = ",result.x) x = result.x # saves result @@ -186,7 +187,7 @@ def main(): plt.xlabel("$f_{\\rm sfr}$") plt.ylabel("$\\log_{10} \\mathcal{L}(f_{\\rm sfr})$") plt.xlim(0,3) - plt.ylim(44,53) + plt.ylim(40,48) ax2 = plt.gca().twinx() l2,=ax2.plot(SFRs,pvalues,color="black",linestyle=":",label="p-value") diff --git a/papers/pathpriors/fit_simple_model.py b/papers/pathpriors/fit_simple_model.py index 23154f14..c4c37c8e 100644 --- a/papers/pathpriors/fit_simple_model.py +++ b/papers/pathpriors/fit_simple_model.py @@ -47,8 +47,6 @@ def main(): ######### List of all ICS FRBs for which we can run PATH ####### # hard-coded list of FRBs with PATH data in ice paper frblist=opt.frblist - frblist.remove('FRB20230731A') # too reddened - # Initlisation of zDM grid # Eventually, this should be part of the loop, i.e. host IDs should @@ -139,7 +137,7 @@ def main(): llstat = on.calculate_likelihood_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUobs,PUprior,plotfile=outfile) ksstat = on.calculate_ks_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,sumPUobs, - sumPUprior,plotfile=outfile,abc="(c)",tag="naive: ",) + sumPUprior,plotfile=outfile,abc="(c)",tag="Naive: ",) print("Best-fit stats of the naive model are ll=",llstat," ks = ",ksstat) diff --git a/papers/pathpriors/get_pu_dist.py b/papers/pathpriors/get_pu_dist.py new file mode 100644 index 00000000..52818871 --- /dev/null +++ b/papers/pathpriors/get_pu_dist.py @@ -0,0 +1,167 @@ +""" +This file fits the simple (naive) model to CRAFT ICS observations. +It fits a model of absolute galaxy magnitude distributions, +uses zDM to predict redshifts and hence apparent magntidues, +runs PATH using that prior, and tries to get priors to match posteriors. + +It also geenrates host z-mr plots + +""" + + +#standard Python imports +import os +import numpy as np +from matplotlib import pyplot as plt +from scipy.optimize import minimize + +# imports from the "FRB" series +from zdm import optical as opt +from zdm import optical_params as op +from zdm import loading +from zdm import cosmology as cos +from zdm import parameters +from zdm import loading +from zdm import optical_numerics as on +from zdm import states + +from importlib import resources + +# other FRB library imports +import astropath.priors as pathpriors + +import matplotlib + +defaultsize=14 +ds=4 +font = {'family' : 'Helvetica', + 'weight' : 'normal', + 'size' : defaultsize} +matplotlib.rc('font', **font) + + +def main(): + """ + Main function + Contains outer loop to iterate over parameters + + """ + ########## Sets optical parameters ######### + + opdir="Plots/" + # Case of simple host model + opstate = op.OpticalState() + + # sets optical state to use simple model + opstate.simple.AbsModelID = 1 # linear interpolation + opstate.simple.AppModelID = 1 # include k-correction + opstate.simple.NModelBins = 6 + opstate.simple.Absmin = -25 + opstate.simple.Absmax = -15 + + # sets the parameters of the P(O|m) function + TELs=["Pan-STARRS","Legacy Surveys","VLT/FORS2"] + TelMeans = [21.8,24.0,26.4] + TelSigmas = [0.54,0.55,0.28] + + + # Initlisation of zDM grid + # Eventually, this should be part of the loop, i.e. host IDs should + # be re-fed into FRB surveys. However, it will be difficult to do this + # with very limited redshift estimates. That might require posterior + # estimates of redshift given the observed galaxies. Maybe. + state = states.load_state("HoffmannHalo25",scat=None,rep=None) + #state = parameters.State() + + labels=['(a) ASKAP/ICS 900 MHz','(b) CHIME ($\delta=60^{\circ}$)','(c) MeerKAT coherent','(d) DSA'] + tags=['ASKAP','CHIME','MeerKAT','DSA'] + names=['CRAFT_ICS_892','CHIME/CHIME_decbin_3_of_6','MeerTRAPcoherent','DSA']#,'CRAFT_ICS_1300','CRAFT_ICS_1632'] + ss,gs = loading.surveys_and_grids(survey_names=names,init_state=state) + + + # initialise figure + styles=[":","--","-"] + NDM=20 + DMlist = np.linspace(50,1950,NDM) + + for j,g in enumerate(gs): + plt.figure() + plt.xlabel("$\\rm DM_{\\rm EG}$ [pc cm$^{-3}$]") + plt.ylabel("$P(U|{\\rm DM_{\\rm EG}})$") + tag = tags[j] + label = labels[j] + plt.text(-250,1.04,label) + for i in np.arange(3): + + opstate.id.pU_mean=TelMeans[i] + opstate.id.pU_width=TelSigmas[i] + + PUs = get_PUs(opstate,g,DMlist) + + if i==0: + l1,=plt.plot(DMlist,PUs[:,0],linestyle=styles[i]) + l2,=plt.plot(DMlist,PUs[:,1],linestyle=styles[i]) + l3,=plt.plot(DMlist,PUs[:,2],linestyle=styles[i]) + elif i==2: + plt.plot(DMlist,PUs[:,0],label="Marnoch23",linestyle=styles[i],color=l1.get_color()) + plt.plot(DMlist,PUs[:,1],label="Loudas25",linestyle=styles[i],color=l2.get_color()) + plt.plot(DMlist,PUs[:,2],label="Naive",linestyle=styles[i],color=l3.get_color()) + else: + plt.plot(DMlist,PUs[:,0],linestyle=styles[i],color=l1.get_color()) + plt.plot(DMlist,PUs[:,1],linestyle=styles[i],color=l2.get_color()) + plt.plot(DMlist,PUs[:,2],linestyle=styles[i],color=l3.get_color()) + # plot kind of optical observation + for i in np.arange(3): + Tlabel=TELs[i] + plt.plot([--100,-50],[-10,-10],color="black",label=Tlabel,linestyle=styles[i]) + + plt.ylim(0,1) + plt.xlim(0,2000) + if j==0: + plt.legend(fontsize=12) + plt.tight_layout() + plt.savefig(opdir+tag+"pu_all.png") + plt.close() + + +def get_PUs(opstate,g,DMlist): + """ + Returns P(U) array for three models + """ + ######### Initialise models ########### + + # model 1: Marnoch + model1 = opt.marnoch_model(opstate) + + # Loudas + model2 = opt.loudas_model(opstate) + xbest = np.load("loudas_output/best_fit_params.npy") + model2.init_args(xbest) + + + model3 = opt.simple_host_model(opstate) + + # retrieve default initial arguments in vector form + xbest = np.load("simple_output/best_fit_params.npy") + model3.init_args(xbest) + + + wrapper1 = opt.model_wrapper(model1,g.zvals) + wrapper2 = opt.model_wrapper(model2,g.zvals) + wrapper3 = opt.model_wrapper(model3,g.zvals) + wrappers = [wrapper1,wrapper2,wrapper3] + + # iterates of DM list + NM=len(wrappers) + NDM = len(DMlist) + PUs = np.zeros([NDM,NM]) + for i,DMEG in enumerate(DMlist): + for j,wrapper in enumerate(wrappers): + wrapper.init_path_raw_prior_Oi(DMEG,g) + PU = wrapper.estimate_unseen_prior() + PUs[i,j]=PU + #print(DMEG,PU) + + return PUs + +main() diff --git a/papers/pathpriors/pO_g_mr/fit_pogmr.py b/papers/pathpriors/pO_g_mr/fit_pogmr.py index 54ce5253..351ae1c1 100644 --- a/papers/pathpriors/pO_g_mr/fit_pogmr.py +++ b/papers/pathpriors/pO_g_mr/fit_pogmr.py @@ -79,7 +79,7 @@ plt.xlim(15,30) plt.ylim(0,1) plt.xlabel("$m_r$") -plt.ylabel("$p(O|m_r)$") +plt.ylabel("$P(O|m_r)$") plt.tight_layout() plt.savefig("pOgm.png") plt.close() diff --git a/papers/pathpriors/plot_colors.py b/papers/pathpriors/plot_colors.py new file mode 100644 index 00000000..5cea5784 --- /dev/null +++ b/papers/pathpriors/plot_colors.py @@ -0,0 +1,70 @@ + +import numpy as np +from matplotlib import pyplot as plt +import matplotlib + +defaultsize=14 +ds=4 +font = {'family' : 'Helvetica', + 'weight' : 'normal', + 'size' : defaultsize} +matplotlib.rc('font', **font) + + + + +# hard-coded color-color data + +# array of g and I bands +gI = [ [21.23,19.875], + [22.24,21.17], + [22.59,21.10], + [18.167,17.097], + [24.02,22.41], + [23.87,22.4], + [21.037,19.618], + [19.103,17.743], + [23.3,21.90], + [21.856,20.61], + [20.910,19.564], + [23.86,22.68], + [20.476,19.194], + [18.128,16.476], + [15.819,14.860], + [17.184,16.212], + [21.49,20.47], + [18.529,17.232]] + +# hard-coded g minus R colours +gR = [[24.02,23.03],[20.842,20.258],[24.22,23.72],[18.529,17.843]] + +RI = [[23.03,22.41],[17.843,17.232]] + +# convert to numpy +gI = np.array(gI) +gR = np.array(gR) +RI = np.array(RI) + + +plt.xlabel("$g-R$") +bins = np.linspace(0,2,21) + + + +print("Mean g minus I is ",np.mean(gI[:,0]-gI[:,1]),gI[:,1].size) +print("Mean R minus I is ",np.mean(RI[:,0]-RI[:,1]),RI[:,1].size) +print("Mean g minus R is ",np.mean(gR[:,0]-gR[:,1]),gR[:,1].size) +plt.figure() +plt.xlim(0.8,1.8) +plt.yticks(np.linspace(0,4,5)) +plt.hist(gI[:,0]-gI[:,1],bins=bins,label="$m_g-m_I$",alpha=0.5) +plt.hist(2.*(gR[:,0]-gR[:,1]),bins=bins,label="$2(m_g-m_R)$",alpha=0.5) +plt.hist(2.*(RI[:,0]-RI[:,1]),bins=bins,label="$2(m_R-m_I)$",alpha=0.5) +plt.legend(loc = "upper left") +plt.xlabel("colour") +plt.ylabel("counts") +plt.tight_layout() +plt.savefig("color_correction.png") +plt.close() + + diff --git a/papers/pathpriors/plot_craft_optical_data.py b/papers/pathpriors/plot_craft_optical_data.py index 9ec8eca5..e18b1047 100644 --- a/papers/pathpriors/plot_craft_optical_data.py +++ b/papers/pathpriors/plot_craft_optical_data.py @@ -1,5 +1,7 @@ """ -This file generates plots of the CRAFT host galaxy candidates +This file generates plots of the CRAFT host galaxy candidates. + +Output is placed in Figures """ diff --git a/papers/pathpriors/plot_host_models.py b/papers/pathpriors/plot_host_models.py index 7535c965..23ec7d17 100644 --- a/papers/pathpriors/plot_host_models.py +++ b/papers/pathpriors/plot_host_models.py @@ -110,15 +110,15 @@ def calc_path_priors(): plt.plot(mrvals,pmr/dmr,label = "Loudas25 ($f_{\\rm sfr}$ = "+str(fsfr)+")", linestyle=styles[j+1]) else: - plt.plot(mrvals,pmr/dmr,linestyle=styles[j+2],\ + plt.plot(mrvals,pmr/dmr,linestyle=styles[j+1],\ color=plt.gca().lines[j+2].get_color()) plt.xlabel("Apparent magnitude $m_r$") plt.ylabel("$P(m_r|z)$") - plt.text(17.5,0.28,"$z=0.1$") - plt.text(22,0.28,"$z=1.0$") + plt.text(17.5,0.285,"$z=0.1$") + plt.text(22,0.285,"$z=1.0$") plt.xlim(12.5,30) plt.ylim(0,0.4) plt.legend(loc="upper right",ncol=2) diff --git a/zdm/optical.py b/zdm/optical.py index 65bd7907..26ffa022 100644 --- a/zdm/optical.py +++ b/zdm/optical.py @@ -841,6 +841,7 @@ def __init__(self,model,zvals): # higher level state defining optical parameters self.OpticalState = model.OpticalState + #parameters defining chance of identifying a galaxy with magnitude m self.pU_mean = self.OpticalState.id.pU_mean self.pU_width = self.OpticalState.id.pU_width @@ -1127,7 +1128,10 @@ def path_raw_prior_Oi(self,mags,ang_sizes,Sigma_ms): # such that \sum priors = 1; here, we need \int priors dm = 1 Oi /= self.dAppmag - Oi /= Sigma_ms[i] # normalise by host counts + # modify sigma_ms by P(m|O) + pogm = (1.-pUgm(mag,self.pU_mean,self.pU_width)) + numerator = Sigma_ms[i] * pogm + Oi /= numerator # normalise by host counts Ois.append(Oi) @@ -1468,7 +1472,7 @@ def matchFRB(TNSname,survey): 'FRB20211212A','FRB20220105A','FRB20220501C', 'FRB20220610A','FRB20220725A','FRB20220918A', 'FRB20221106A','FRB20230526A','FRB20230708A', - 'FRB20230731A','FRB20230902A','FRB20231226A','FRB20240201A', + 'FRB20230902A','FRB20231226A','FRB20240201A', 'FRB20240210A','FRB20240304A','FRB20240310A'] diff --git a/zdm/optical_numerics.py b/zdm/optical_numerics.py index e6664062..902ce5c5 100644 --- a/zdm/optical_numerics.py +++ b/zdm/optical_numerics.py @@ -399,7 +399,8 @@ def calculate_ks_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUobs return stat def make_cumulative_plots(NMODELS,NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUobs, - PUprior,plotfile,plotlabel,POxcut=None,abc=None,onlyobs=None): + PUprior,plotfile,plotlabel,POxcut=None,abc=None,onlyobs=None, + greyscale=[],addpriorlabel=True): """ Creates cumulative plots of KS-like behaviour for multiple fit outcomes @@ -419,7 +420,8 @@ def make_cumulative_plots(NMODELS,NFRB,AppMags,AppMagPriors,ObsMags,ObsPosterior PUobs: posterior on unseen probability PUprior: prior on PU POxcut: if not None, cut data to fixed POx. Used to simulate current techniques - + greyscale (list of ints): if present, defines which models to plot as background greyscales + addpriorlabel (bool): if True, add "prior" to label of priors Returns: None """ @@ -427,6 +429,7 @@ def make_cumulative_plots(NMODELS,NFRB,AppMags,AppMagPriors,ObsMags,ObsPosterior # arrays to hold created observed and prior distributions prior_dists = [] obs_dists = [] + linestyles=[":","--","-.","-"] # loops over models to create prior distributions for imodel in np.arange(NMODELS): @@ -479,6 +482,24 @@ def make_cumulative_plots(NMODELS,NFRB,AppMags,AppMagPriors,ObsMags,ObsPosterior for imodel in np.arange(NMODELS): + if onlyobs is None or onlyobs == imodel: + if onlyobs is not None: + color = 'black' + label = "Observed" # don't sub-label, since this stands in for all observed + else: + color=plt.gca().lines[-1].get_color() + label = plotlabel[imodel]+": Observed" + + plt.plot(AppMags[imodel],obs_dists[imodel],label=label, + color=color) + + # adds gryescale 'background' plots of observed distributions + if imodel in greyscale: + plt.plot(AppMags[imodel],obs_dists[imodel],color="gray") + # add these in greyscale, to highlight they are 'background' plots + # this option never used, but experimented with. + + # calcs lowest x that is essentially at max ixmax = np.where(prior_dist > prior_dist[-1]*0.999)[0][0] # rounds it up to multiple of 5 @@ -490,28 +511,25 @@ def make_cumulative_plots(NMODELS,NFRB,AppMags,AppMagPriors,ObsMags,ObsPosterior plt.xlim(xmin,xmax) #cx,cy = make_cdf_for_plotting(ObsMags,weights=ObsPosteriors) - plt.plot(AppMags[imodel],prior_dists[imodel],label=plotlabel[imodel]+": Prior", - linestyle=":") - if onlyobs is None or onlyobs == imodel: - if onlyobs is not None: - color='black' - else: - color=plt.gca().lines[-1].get_color() - plt.plot(AppMags[imodel],obs_dists[imodel],label=plotlabel[imodel]+": Observed", - color=color) + if addpriorlabel: + label = plotlabel[imodel]+": Prior" + else: + label = plotlabel[imodel] + + plt.plot(AppMags[imodel],prior_dists[imodel],label=label, + linestyle=linestyles[imodel%4]) + if abc is not None: plt.text(0.02,0.9,abc,fontsize=16, transform=plt.gcf().transFigure) - plt.legend(loc="upper left") + plt.legend(fontsize=12,loc="upper left") plt.tight_layout() plt.savefig(plotfile) plt.close() return None - - def get_cand_properties(frblist): """ Returns properties of galaxy candidates for FRBs @@ -537,7 +555,7 @@ def get_cand_properties(frblist): candidates = ptbl[['ang_size', 'mag', 'ra', 'dec', 'separation']] all_candidates.append(candidates) return all_candidates - + def run_path(name,P_U=0.1,usemodel = False, sort=False): """ evaluates PATH on an FRB @@ -554,14 +572,6 @@ def run_path(name,P_U=0.1,usemodel = False, sort=False): my_frb = FRB.by_name(name) this_path = frbassociate.FRBAssociate(my_frb, max_radius=10.) - - # do NOT do the below method! - # - - # do NOT do the below!! - #my_frb.set_ee(my_frb.sig_a,my_frb.sig_b,my_frb.eellipse['theta'], - # my_frb.eellipse['cl'],True) - # reads in galaxy info ppath = os.path.join(resources.files('frb'), 'data', 'Galaxies', 'PATH') pfile = os.path.join(ppath, f'{my_frb.frb_name}_PATH.csv') @@ -584,12 +594,27 @@ def run_path(name,P_U=0.1,usemodel = False, sort=False): candidates = ptbl[['ang_size', 'mag', 'ra', 'dec', 'separation']] + # implements a correction to their relative magnitudes. + # note that order is R, then I, then g + if "VLT_FORS2_R" in ptbl: + mags = np.array(candidates.mag.values) + elif "VLT_FORS2_I" in ptbl: + mags = np.array(candidates.mag.values) + 0.65 + elif "VLT_FORS2_g" in ptbl: + mags = np.array(candidates.mag.values) - 0.65 + elif "GMOS_S_i" in ptbl: + mags = np.array(candidates.mag.values) + 0.65 + elif "LRIS_I" in ptbl: + mags = np.array(candidates.mag.values) + 0.65 + else: + raise ValueError("Cannot implement colour correction") + #this_path = PATH() this_path.init_candidates(candidates.ra.values, candidates.dec.values, candidates.ang_size.values, - mag=candidates.mag.values) + mag=mags) this_path.frb = my_frb frb_eellipse = dict(a=np.abs(my_frb.sig_a), @@ -621,7 +646,8 @@ def run_path(name,P_U=0.1,usemodel = False, sort=False): max_radius=10., debug=debug) - mags = candidates['mag'] + # mags already defined above + #mags = candidates['mag'] if sort: indices = np.argsort(P_Ox) From 519b9bbd5c0b3e2deef4c63ea8ee0e88817820af Mon Sep 17 00:00:00 2001 From: "J. Xavier Prochaska" Date: Thu, 19 Mar 2026 12:55:44 +0900 Subject: [PATCH 24/35] lots o docs --- zdm/grid.py | 40 +- zdm/optical.py | 575 +++++++++++++++-------- zdm/scripts/Path/estimate_path_priors.py | 67 ++- zdm/scripts/Path/optimise_host_priors.py | 118 ++++- zdm/scripts/Path/plot_host_models.py | 91 +++- 5 files changed, 650 insertions(+), 241 deletions(-) diff --git a/zdm/grid.py b/zdm/grid.py index be1ef34e..dea68aac 100644 --- a/zdm/grid.py +++ b/zdm/grid.py @@ -1260,7 +1260,45 @@ def chk_upd_param(self, param: str, vparams: dict, update=False): def smear_z(self,array,zsigma): """ - smears the z-grid according to a specified photometric error + Smear a 2-D z-DM grid along the redshift axis to account for + photometric redshift uncertainty. + + When a survey uses photometric rather than spectroscopic redshifts, + the true redshift of each FRB host is uncertain. This method convolves + each column of the grid (i.e. each fixed-DM slice along the z axis) + with a Gaussian kernel whose standard deviation equals ``zsigma``, + redistributing probability across neighbouring redshift bins. + + The kernel is truncated at ``state.photo.sigma_width`` standard + deviations on each side (default 6σ), rounded up to an odd number of + bins so that it is centred exactly on zero. + + Parameters + ---------- + array : np.ndarray, shape (Nz, NDM) + Input 2-D grid with redshift along axis 0 and DM along axis 1. + zsigma : float + Photometric redshift uncertainty (1σ), in the same units as + ``self.zvals`` (i.e. dimensionless redshift). + + Returns + ------- + smear_zgrid : np.ndarray, shape (Nz, NDM) + Copy of ``array`` with each DM column convolved along the z axis + by the Gaussian smearing kernel. Boundary effects are handled with + ``np.convolve`` mode ``"same"``, so the output has the same shape + as the input. + + Notes + ----- + The kernel width in grid bins is ``zsigma / self.dz``. Values near the + grid edges will be underestimated because the convolution truncates to + zero outside the grid; for well-chosen grid extents this edge effect is + negligible. + + In ``calc_rates``, this method is called with ``zsigma`` taken from + ``self.survey.survey_data.observing.Z_PHOTO`` and applied to + ``self.rates`` after the FRB rate grid has been computed. """ r,c=array.shape # get sigma in grid units diff --git a/zdm/optical.py b/zdm/optical.py index 26ffa022..eaa398c1 100644 --- a/zdm/optical.py +++ b/zdm/optical.py @@ -1,47 +1,58 @@ """ -This library contains routines that interact with -the FRB/astropath module and (optical) FRB host galaxy -information. +Optical FRB host galaxy models and PATH interface for zdm. -The philosophy of the module is this. The base class -is "host_model". This class is the top-level class -that contains base functions to e.g. calculate -p(m_r|DM). +This module connects zdm redshift-DM grids with the PATH +(Probabilistic Association of Transients to their Hosts) algorithm by +providing physically motivated priors on FRB host galaxy apparent +magnitudes, p(m_r | DM_EG). -However, no host_model class contains any astroiphysics. +Architecture +------------ +The module is built around a two-layer design: -Instead, it wraps an underlying set of possible class -objects that must have a specific set of callable functions -which each contain the relevant calculations. +**Host magnitude models** — each describes the intrinsic absolute +magnitude distribution of FRB host galaxies, p(M_r), and can convert +it to an apparent magnitude distribution p(m_r | z) at a given +redshift. Three models are provided: -The current set are the following: +- ``simple_host_model``: parametric histogram of p(M_r) with N free + amplitudes (default 10), interpolated linearly or via spline. An + optional power-law k-correction can be included. N (or N+1) + free parameters. -Simple_host_model: - Describes intrinsic host properties as a spline - interpolation between p(M_r) described by N - points. N parameters (e.g. 10). +- ``loudas_model``: precomputed p(m_r | z) tables from Nick Loudas, + constructed by weighting galaxies by stellar mass or star-formation + rate. Interpolated between tabulated redshift bins with a + luminosity-distance shift. Single free parameter ``fSFR`` sets the + SFR/mass mixing fraction. -Marnoch_model: - Fixed calculation of p(M_r) based on extrapolation - of known FRB host galaxies. No parameters. See - https://doi.org/10.1093/mnras/stad2353 - +- ``marnoch_model``: zero-parameter model. Fits a Gaussian to the + r-band magnitude distribution of known CRAFT ICS host galaxies from + Marnoch et al. 2023 (MNRAS 525, 994), using cubic splines for the + redshift-dependent mean and standard deviation. -Loudas_model: - Calculates p(M_r) via assigning a fraction of FRB - hosts to follow star-formation in galaxies, and - a fraction to stellar mass, then includes the modelled - evolution of these galaxies. 1 parameter. - -Each "host_model" class object above must provide functions to: - __init__ - calculate p(m_r|z,parameters) - -The wrapper class provides the following fubctions: - -- init_path_raw_prior_Oi(self,DM,grid): - (takes as input an FRB DM, and grid object) +**``model_wrapper``** — a survey-independent wrapper around any host +model. Given a zdm ``Grid`` and an observed DM_EG, it convolves +p(m_r | z) with the zdm p(z | DM_EG) posterior to produce a +DM-informed apparent magnitude prior for PATH. It also estimates +P_U, the prior probability that the true host is below the survey +detection limit. + +Typical usage +------------- +:: + + model = opt.marnoch_model() + wrapper = opt.model_wrapper(model, grid.zvals) + wrapper.init_path_raw_prior_Oi(DMEG, grid) # DM-specific initialisation + PU = wrapper.estimate_unseen_prior() + # pathpriors.USR_raw_prior_Oi is now set automatically by init_path_raw_prior_Oi +Module-level data +----------------- +``frblist`` : list of str + TNS names of CRAFT ICS FRBs for which PATH optical data are + available (used by the scripts in ``zdm/scripts/Path/``). """ @@ -109,7 +120,19 @@ def load_data(self): def process_rbands(self): """ - Returns parameters of the host magnitude distribution as a function of redshift + Build cubic spline fits to the mean and rms of p(m_r) as a function of z. + + Reads the per-FRB r-band magnitude columns from ``self.table``, + computes their mean (``Rbar``) and sample standard deviation (``Rrms``) + across all FRBs at each tabulated redshift, then fits two cubic splines: + + - ``self.sbar``: CubicSpline interpolating the mean apparent magnitude + as a function of redshift. + - ``self.srms``: CubicSpline interpolating the rms scatter as a + function of redshift. + + These splines are subsequently used by ``get_pmr_gz`` to evaluate + the Gaussian p(m_r | z) at arbitrary redshifts. """ table = self.table @@ -138,15 +161,32 @@ def process_rbands(self): #return Rbar,Rrms,zlist,sbar,srms - def get_pmr_gz(self,mrbins,z): # fsfr must be a self value z: float,fsfr: float): + def get_pmr_gz(self,mrbins,z): """ - Returns the p_mr distribution for a given redshift z and sfr fraction f_sfr - - Args: - mrbins (array of floats): list of r-band magnitude bins - z (float): redshift + Return the apparent magnitude probability distribution p(m_r | z). + + Evaluates a Gaussian distribution whose mean and standard deviation + are obtained from the cubic splines fit in ``process_rbands``, + and integrates it over the provided magnitude bins. + + This model has no free parameters; the Gaussian moments are fully + determined by the Marnoch et al. 2023 host galaxy data. + + Parameters + ---------- + mrbins : array-like of float, length N+1 + Edges of the apparent magnitude bins over which to compute the + probability. The output has length N (one value per bin). + z : float + Redshift at which to evaluate the magnitude distribution. + + Returns + ------- + pmr : np.ndarray, length N + Probability in each magnitude bin (sums to ≤ 1; may be less + than unity if the Gaussian extends beyond the bin range). """ - + mean = self.sbar(z) rms = self.srms(z) @@ -169,12 +209,16 @@ class loudas_model: def __init__(self,OpticalState=None,fname='p_mr_distributions_dz0.01_z_in_0_1.2.h5',data_dir=None,verbose=False): """ - initialises the model. Loads data provided by Nick Loudas - on mass- and sfr-weighted magnitudes. - + Initialise the Loudas model, loading precomputed p(m_r | z) tables. + Args: - fname [string]: h55 filename containing the data - datadir [string]: directory that the data is contained in. Defaults to None. + OpticalState (OpticalState, optional): optical parameter state. A + default ``OpticalState`` is created if not provided. + fname (str): HDF5 filename containing the Loudas p(m_r | z) tables. + Defaults to ``'p_mr_distributions_dz0.01_z_in_0_1.2.h5'``. + data_dir (str, optional): directory containing ``fname``. Defaults + to the package data directory ``zdm/data/optical/``. + verbose (bool): if True, print progress messages. Defaults to False. """ # uses the "simple hosts" descriptor @@ -246,15 +290,31 @@ def init_cubics(self): self.mass_splines = mass_splines self.sfr_splines = sfr_splines - def get_pmr_gz(self,mrbins,z): # fsfr must be a self value z: float,fsfr: float): + def get_pmr_gz(self,mrbins,z): """ - Returns the p_mr distribution for a given redshift z and sfr fraction f_sfr - Should be defined such that the sum over all mrbins is unity (or less, - if there is a limitation due to range) - - Args: - z (float): redshift - fsfr (float): fraction of population associated with star-formation + Return the apparent magnitude probability distribution p(m_r | z). + + Interpolates between the two nearest tabulated redshift bins (in + log-z space), applying a luminosity-distance shift to each tabulated + p(m_r) before combining them. The mass- and SFR-weighted distributions + are mixed according to ``self.fsfr`` (set via ``init_args``). + + The result is normalised so that the bin probabilities sum to unity + over the full magnitude range, provided the distribution does not + extend significantly beyond ``mrbins``. + + Parameters + ---------- + mrbins : array-like of float, length N+1 + Edges of the apparent magnitude bins. Output has length N. + z : float + Redshift at which to evaluate the distribution. Values outside + the tabulated range are extrapolated from the nearest edge bin. + + Returns + ------- + pmr : np.ndarray, length N + Probability in each apparent magnitude bin (sums to ≤ 1). """ fsfr = self.fsfr @@ -383,15 +443,18 @@ def load_p_mr_distributions(self,data_dir,fname: str = 'p_mr_distributions_dz0.0 def give_p_mr_mass(self,z: float): """ - Function to return p(mr|z) for mass-weighted population. + Return p(m_r | z) for the stellar-mass-weighted host population. + + Uses a nearest-bin lookup (no interpolation) in the tabulated redshift + grid. For interpolated results with luminosity-distance shifting, use + ``get_pmr_gz`` with ``fSFR=0`` instead. + Args: z (float): Redshift value. + Returns: - np.array: p(mr|z) values. - Note: - This function assumes that the redshift bins are defined in the `massweighted_population` data. - Given the fine discretization of redshift bins, it uses the nearest bin for the provided redshift value. - rmag_centers and p_mr_mass are defined in the outer scope of this function. + np.ndarray: p(m_r | z) values at the tabulated r-band magnitude + centres (``self.rmags``), normalised to sum to unity. """ # Find the appropriate redshift bin index idx = np.clip(np.searchsorted(self.zbins, z) - 1, 0, n_redshift_bins - 1) @@ -399,15 +462,18 @@ def give_p_mr_mass(self,z: float): def give_p_mr_sfr(self,z: float): """ - Function to return p(mr|z) for SFR-weighted population. + Return p(m_r | z) for the star-formation-rate-weighted host population. + + Uses a nearest-bin lookup (no interpolation) in the tabulated redshift + grid. For interpolated results with luminosity-distance shifting, use + ``get_pmr_gz`` with ``fSFR=1`` instead. + Args: z (float): Redshift value. + Returns: - np.array: p(mr|z) values. - Note: - This function assumes that the redshift bins are defined in the `sfrweighted_population` data. - Given the fine discretization of redshift bins, it uses the nearest bin for the provided redshift value. - rmag_centers and p_mr_sfr are defined in the outer scope of this function. + np.ndarray: p(m_r | z) values at the tabulated r-band magnitude + centres (``self.rmags``), normalised to sum to unity. """ # Find the appropriate redshift bin index idx = np.clip(np.searchsorted(self.zbins, z) - 1, 0, n_redshift_bins - 1) @@ -415,11 +481,14 @@ def give_p_mr_sfr(self,z: float): def init_args(self,fSFR): """ - Initialises prior based on sfr fraction - + Set the SFR/mass mixing fraction for the Loudas model. + Args: - opstate: optical model state. Grabs the Loudas parameters from there. - + fSFR (float or array-like of length 1): fraction of FRB hosts + that trace star-formation rate. ``fSFR=0`` gives a purely + mass-weighted population; ``fSFR=1`` gives a purely + SFR-weighted population. Intermediate values linearly mix + the two. If an array is passed, only the first element is used. """ # for numerical purposes, fSFR may have to be a vector if hasattr(fSFR,'__len__'): @@ -475,13 +544,15 @@ class simple_host_model: """ def __init__(self,OpticalState=None,verbose=False): """ - Class constructor. - + Initialise the simple host magnitude model. + Args: - opstate (class: Hosts, optional): class defining parameters - of optical state model - verbose (bool, optional): to be verbose y/n - + OpticalState (OpticalState, optional): optical parameter state + providing model configuration (magnitude ranges, number of + bins, interpolation scheme, k-correction flag). A default + ``OpticalState`` is created if not provided. + verbose (bool, optional): if True, print which sub-models are + being initialised. Defaults to False. """ # uses the "simple hosts" descriptor if OpticalState is None: @@ -647,25 +718,35 @@ def init_model_bins(self): def get_pmr_gz(self,mrbins,z): """ - For a set of redshifts, initialise mapping - between intrinsic magnitudes and apparent magnitudes - - This routine only needs to be called once, since the model - to convert absolute to apparent magnitudes is fixed - - It is not set automatically however, and needs to be called - with a set of z values. This is all for speedup purposes. - - Args: - mrbins (np.array, float, length N+1): array of apparent magnitudes (mr) - over which to calculate p(mr). These act as bins - in apparent magnitude mr for histogram purposes, - i.e. they are not probabilities *at* mr - zvals (float): redshifts at which - to map absolute to apparent magnitudes. - - Returns: - pmr: probability for each of the bins (length: N) + Return the apparent magnitude probability distribution p(m_r | z). + + Converts each apparent magnitude bin centre back to an absolute + magnitude using ``CalcAbsoluteMags``, then linearly interpolates + the absolute magnitude weight array (``self.AbsMagWeights``) to + obtain a probability density at each bin. + + Parameters + ---------- + mrbins : np.ndarray of float, length N+1 + Edges of the apparent magnitude bins. The output has length N, + with one probability value per bin centre. + z : float + Redshift at which to evaluate the distribution. + + Returns + ------- + pmr : np.ndarray, length N + Probability density at each apparent magnitude bin centre. + Values at the edges of the absolute magnitude range are + clamped to the nearest valid bin rather than extrapolated. + + Notes + ----- + The returned values are NOT renormalised to sum to unity; the sum + may be less than one if some absolute magnitudes lie outside the + model range ``[Absmin, Absmax]``. This is intentional: the + shortfall represents probability mass for hosts too faint or too + bright to appear in the apparent magnitude range. """ old = False @@ -921,23 +1002,28 @@ def init_zmapping(self,zvals): def init_path_raw_prior_Oi(self,DM,grid): """ - Initialises the priors for a particlar DM. - This performs a function very similar to - "get_posterior" except that it expicitly - only operates on a single DM, and saves the - information internally so that - path_raw_prior_Oi can be called for numerous - host galaxy candidates. - - It returns the priors distribution. - + Initialise the apparent magnitude prior for a single FRB DM. + + Computes p(m_r | DM_EG) by convolving the precomputed p(m_r | z) + grid (``self.p_mr_z``) with the zdm posterior p(z | DM_EG) extracted + from ``grid``. The result is stored internally so that + ``path_raw_prior_Oi`` can be called repeatedly for different host + galaxy candidates belonging to the same FRB without recomputing the + DM integral. + + Also computes the probability that the host is undetected: + - ``self.priors``: p(m_r | DM) weighted by the detection probability + p(detected | m_r). + - ``self.PUdist``: the magnitude-resolved contribution to P_U. + - ``self.PU``: scalar total prior probability that the host is unseen, + returned by ``estimate_unseen_prior()``. + + After this call, ``pathpriors.USR_raw_prior_Oi`` is automatically + pointed at ``self.path_raw_prior_Oi``. + Args: - DM [float]: dispersion measure of an FRB (pc cm-3) - grid (class grid): initialised grid object from which - to calculate priors - - Returns: - priors (float): vector of priors on host galaxy apparent magnitude + DM (float): extragalactic dispersion measure of the FRB (pc cm⁻³). + grid (Grid): initialised zdm grid object providing p(z, DM). """ # we start by getting the posterior distribution p(z) @@ -966,20 +1052,26 @@ def init_path_raw_prior_Oi(self,DM,grid): def get_posterior(self, grid, DM): """ - Similar functionality to init_path_raw_prior_Oi. May be legacy code. - - Returns posterior redshift distributiuon for a given grid, and DM - magnitude distribution, for FRBs of DM given a grid object. - Note: this calculates a prior for PATH, but is a posterior - from zDM's point of view. - + Return apparent magnitude and redshift posteriors for a given DM. + + Computes p(z | DM) from the grid and convolves it with the + precomputed ``self.maghist`` to obtain p(m_r | DM). + + Note: from PATH's perspective this is a prior on host magnitude, + but from zdm's perspective it is a posterior on redshift given DM. + + This method predates ``init_path_raw_prior_Oi`` and may not be + actively used in current scripts. + Args: - grid (class grid object): grid object defining p(z,DM) - DM (float, np.ndarray OR scalar): FRB DM(s) - + grid (Grid): initialised zdm grid object providing p(z, DM). + DM (float or np.ndarray): FRB extragalactic DM(s) in pc cm⁻³. + Returns: - papps (np.ndarray, floats): probability distribution of apparent magnitudes given DM - pz (np.ndarray, floats): probability distribution of redshift given DM + papps (np.ndarray): probability distribution of apparent magnitude + given DM, p(m_r | DM). + pz (np.ndarray): probability distribution of redshift given DM, + p(z | DM). """ # Step 1: get prior on z pz = get_pz_prior(grid,DM) @@ -1033,8 +1125,24 @@ def estimate_unseen_prior(self): def path_base_prior(self,mags): """ - Calculates base magnitude prior. Does NOT include - galaxy density factor + Evaluate the apparent magnitude prior p(m_r | DM) at a list of magnitudes. + + Linearly interpolates ``self.priors`` (which already incorporates the + detection probability p(detected | m_r)) at each requested magnitude, + converting from the internally normalised sum-to-unity convention to a + probability density by dividing by the bin width ``self.dAppmag``. + + Unlike ``path_raw_prior_Oi``, this method does NOT divide by the + galaxy surface density Sigma_m, so it returns the raw magnitude prior + without the PATH normalisation factor. + + Args: + mags (list or tuple of float): apparent r-band magnitudes of + candidate host galaxies at which to evaluate the prior. + + Returns: + Ois (np.ndarray): prior probability density p(m_r | DM) evaluated + at each magnitude in ``mags``. """ ngals = len(mags) Ois = [] @@ -1189,22 +1297,22 @@ def get_pz_prior(grid, DM): def SimplekApparentMags(Abs,k,zs): """ - Function to convert galaxy absolue to apparent magnitudes. - Same as simple apparent mags, but allows for a k-correction. - - Nominally, magnitudes are r-band magnitudes, but this function - is so simple it doesn't matter. - - Just applies a distance correction - no k-correction. - + Convert absolute to apparent magnitudes with a power-law k-correction. + + Applies the distance modulus plus a k-correction of the form + ``2.5 * k * log10(1 + z)``. + Args: - Abs (float or array of floats): intrinsic galaxy luminosities - k (float): k-correction - zs (float or array of floats): redshifts of galaxies - + Abs (float or np.ndarray): absolute magnitude(s) M_r. + k (float): k-correction power-law index. ``k=0`` reduces to a + pure distance modulus (identical to ``SimpleApparentMags``). + zs (float or np.ndarray): redshift(s) of the galaxies. + Returns: - ApparentMags: NAbs x NZ array of magnitudes, where these - are the dimensions of the inputs + ApparentMags: apparent magnitude(s). Scalar if both inputs are + scalar; 1-D array if one is scalar and one is an array; 2-D + array of shape (NAbs, Nz) if both are arrays (computed via + ``np.outer``). """ # calculates luminosity distances (Mpc) @@ -1239,22 +1347,22 @@ def SimplekApparentMags(Abs,k,zs): def SimplekAbsoluteMags(App,k,zs): """ - Function to convert galaxy apparent to absolute magnitudes. - Same as simple absolute mags mags, but allows for a k-correction. - - Nominally, magnitudes are r-band magnitudes, but this function - is so simple it doesn't matter. - - Just applies a distance correction - no k-correction. - + Convert apparent to absolute magnitudes with a power-law k-correction. + + Inverse of ``SimplekApparentMags``: subtracts the distance modulus and + k-correction ``2.5 * k * log10(1 + z)`` from the apparent magnitude. + Args: - App (float or array of floats): apparent galaxy luminosities - k (float): k-correction - zs (float or array of floats): redshifts of galaxies - + App (float or np.ndarray): apparent magnitude(s) m_r. + k (float): k-correction power-law index. ``k=0`` reduces to a + pure distance modulus (identical to ``SimpleAbsoluteMags``). + zs (float or np.ndarray): redshift(s) of the galaxies. + Returns: - AbsoluteMags: NAbs x NZ array of magnitudes, where these - are the dimensions of the inputs + AbsoluteMags: absolute magnitude(s). Scalar if both inputs are + scalar; 1-D array if one is scalar and one is an array; 2-D + array of shape (NApp, Nz) if both are arrays (computed via + ``np.outer``). """ # calculates luminosity distances (Mpc) @@ -1289,20 +1397,20 @@ def SimplekAbsoluteMags(App,k,zs): def SimpleAbsoluteMags(App,zs): """ - Function to convert galaxy apparent to absolute magnitudes. - - Nominally, magnitudes are r-band magnitudes, but this function - is so simple it doesn't matter. - - Just applies a distance correction - no k-correction. - + Convert apparent to absolute magnitudes using the distance modulus only. + + Subtracts ``5 * log10(D_L / 10 pc)`` from the apparent magnitude, where + D_L is the luminosity distance in Mpc. No k-correction is applied. + Args: - App (float or array of floats): apparent galaxy luminosities - zs (float or array of floats): redshifts of galaxies - + App (float or np.ndarray): apparent magnitude(s) m_r. + zs (float or np.ndarray): redshift(s) of the galaxies. + Returns: - AbsoluteMags: NAbs x NZ array of magnitudes, where these - are the dimensions of the inputs + AbsoluteMags: absolute magnitude(s). Scalar if both inputs are + scalar; 1-D array if one input is scalar and one is an array; + 2-D array of shape (NApp, Nz) if both are arrays (computed via + ``np.outer``). """ # calculates luminosity distances (Mpc) @@ -1332,20 +1440,20 @@ def SimpleAbsoluteMags(App,zs): def SimpleApparentMags(Abs,zs): """ - Function to convert galaxy absolue to apparent magnitudes. - - Nominally, magnitudes are r-band magnitudes, but this function - is so simple it doesn't matter. - - Just applies a distance correction - no k-correction. - + Convert absolute to apparent magnitudes using the distance modulus only. + + Adds ``5 * log10(D_L / 10 pc)`` to the absolute magnitude, where + D_L is the luminosity distance in Mpc. No k-correction is applied. + Args: - Abs (float or array of floats): intrinsic galaxy luminosities - zs (float or array of floats): redshifts of galaxies - + Abs (float or np.ndarray): absolute magnitude(s) M_r. + zs (float or np.ndarray): redshift(s) of the galaxies. + Returns: - ApparentMags: NAbs x NZ array of magnitudes, where these - are the dimensions of the inputs + ApparentMags: apparent magnitude(s). Scalar if both inputs are + scalar; 1-D array if one input is scalar and one is an array; + 2-D array of shape (NAbs, Nz) if both are arrays (computed via + ``np.outer``). """ # calculates luminosity distances (Mpc) @@ -1373,15 +1481,23 @@ def SimpleApparentMags(Abs,zs): def p_unseen_Marnoch(zvals,plot=False): """ - Returns probability of a hist being unseen in typical VLT - observations. - - Inputs: - zvals [float, array]: array of redshifts - + Return the probability that an FRB host galaxy is unseen in typical VLT observations. + + Digitises Figure 3 of Marnoch et al. 2023 (MNRAS 525, 994), which shows + p(U | z) — the cumulative probability that a host galaxy at redshift z + falls below the VLT/FORS2 R-band detection limit. A cubic polynomial is + fit to the digitised curve and evaluated at the requested redshifts. + Values are clamped to [0, 1]. + + Args: + zvals (float or np.ndarray): redshift(s) at which to evaluate p(U | z). + plot (bool): if True, save a diagnostic comparison plot of the raw + digitised data, linear interpolation, and polynomial fit to + ``p_unseen.pdf``. Defaults to False. + Returns: - fitv [float, array]: p(Unseen) for redshift zvals - + fitv (np.ndarray): p(U | z) evaluated at each element of ``zvals``, + clamped to the range [0, 1]. """ # approx digitisation of Figure 3 p(U|z) # from Marnoch et al. @@ -1426,7 +1542,20 @@ def p_unseen_Marnoch(zvals,plot=False): def simplify_name(TNSname): """ - Simplifies an FRB name to basics + Reduce a TNS FRB name to a six-character YYMMDD[L] identifier. + + Strips the leading ``FRB`` prefix (if present) and the year's + century digits, retaining only the six-digit date plus any + trailing letter suffix, to allow case-insensitive matching + between survey entries and external FRB catalogues. + + Args: + TNSname (str): FRB name in TNS format, e.g. ``'FRB20180924B'`` + or ``'20180924B'``. + + Returns: + name (str): simplified six-character identifier, e.g. ``'180924B'`` + (six digits plus optional letter). """ # reduces all FRBs to six integers @@ -1447,10 +1576,21 @@ def simplify_name(TNSname): def matchFRB(TNSname,survey): """ - Gets the FRB id from the survey list - Returns None if not in the survey - Used to match properties between a survey - and other FRB libraries + Find the index of an FRB in a survey by TNS name. + + Uses ``simplify_name`` to normalise both the query name and the survey + entries, so minor formatting differences (century digits, trailing + letters) do not prevent a match. + + Args: + TNSname (str): TNS name of the FRB to look up, e.g. + ``'FRB20180924B'``. + survey (Survey): loaded survey object whose ``frbs["TNS"]`` column + contains TNS names of detected FRBs. + + Returns: + int or None: index into ``survey.frbs`` of the first matching FRB, + or ``None`` if the FRB is not found in the survey. """ name = simplify_name(TNSname) @@ -1480,13 +1620,24 @@ def matchFRB(TNSname,survey): def plot_frb(name,ralist,declist,plist,opfile): """ - does an frb - - absolute [bool]: if True, treats rel_error as an absolute value - in arcseconds - - clist: list of astropy coordinates - plist: list of p(O|x) for candidates hosts + Plot an FRB localisation and its PATH host galaxy candidates. + + Produces a scatter plot showing the FRB position and a set of + deviated/sampled positions colour-coded by their PATH posterior + P(O|x), overlaid with circles representing candidate host galaxies + scaled by their angular size. All coordinates are shown in arcseconds + relative to the FRB position. + + Args: + name (str): TNS FRB name (e.g. ``'FRB20180924B'``), used to load + the FRB object and the corresponding PATH candidate table. + ralist (np.ndarray): right ascension values (degrees) of deviated + FRB positions to plot, colour-coded by ``plist``. + declist (np.ndarray): declination values (degrees) of deviated + FRB positions. + plist (np.ndarray): PATH posterior values P(O|x) for the deviated + positions, used to set the colour scale. + opfile (str): output file path for the saved figure. """ from frb.frb import FRB @@ -1543,15 +1694,25 @@ def plot_frb(name,ralist,declist,plist,opfile): def pUgm(mag,mean,width): """ - Function to describe probability of a galaxy being unidentified - in an optical image as a function of its magnitude - + Return the probability that a galaxy is undetected as a function of magnitude. + + Models the survey detection completeness as a logistic function that + transitions from ~0 (bright, always detected) to ~1 (faint, never + detected) with a smooth rolloff centred on ``mean``: + + p(U | m) = 1 / (1 + exp((mean - m) / width)) + Args: - mag (float or array of floats): magnitude(s) at which - to evaluate the function - mean: magnitude at which the probability is 50% - width: characteristic width of transition from 0-50 and - 50-100 % + mag (float or np.ndarray): r-band apparent magnitude(s) at which to + evaluate the detection-failure probability. + mean (float): magnitude at which p(U | m) = 0.5 (the 50% completeness + limit of the survey). + width (float): characteristic width of the completeness rolloff in + magnitudes. Smaller values give a sharper transition. + + Returns: + pU (float or np.ndarray): probability of non-detection at each + magnitude in ``mag``, in the range [0, 1]. """ # converts to a number relative to the mean. Will be weird for mags < 0. diff --git a/zdm/scripts/Path/estimate_path_priors.py b/zdm/scripts/Path/estimate_path_priors.py index 36cf0d84..b3b1155e 100644 --- a/zdm/scripts/Path/estimate_path_priors.py +++ b/zdm/scripts/Path/estimate_path_priors.py @@ -1,10 +1,39 @@ """ -Script showing how to use zDM as priors for CRAFT -host galaxy magnitudes. +Estimate zdm-informed PATH priors for CRAFT/ICS FRB host galaxies. -It requirses the FRB and astropath modules to be installed. +This script demonstrates how to incorporate zdm-derived p(z|DM) predictions +as priors for the PATH (Probabilistic Association of Transients to their Hosts) +algorithm applied to CRAFT ICS FRBs. -This does NOT include optimisation of any parameters +For each FRB in the CRAFT ICS sample (`opt.frblist`), the script runs PATH +twice and compares results: + +1. **Baseline run**: PATH with a flat (uninformative) prior on host galaxy + apparent magnitude, and a fixed prior P_U=0.1 on the host being below + the detection threshold. + +2. **zdm-informed run**: PATH using a physically motivated prior on host + apparent magnitude derived from the Marnoch+2023 host galaxy luminosity + model combined with the zdm p(z|DM_EG) probability distribution. The + probability P_U that the true host is undetected is also estimated from + the model rather than set by hand. + +The output is a weighted histogram of posterior host galaxy apparent +magnitudes (P_Ox) across all FRBs, saved to ``posterior_pOx.png``. + +Note: This script does NOT optimise any zdm or host galaxy model parameters. +It uses the CRAFT_ICS_1300 survey grid with default zdm parameter values. + +Requirements +------------ +- ``astropath`` package (PATH implementation) +- ``frb`` package (FRB utilities and optical data) +- PATH-compatible optical data for each FRB in ``opt.frblist`` + +References +---------- +- Marnoch et al. 2023, MNRAS 525, 994 (host galaxy luminosity model) +- Macquart et al. 2020 (Macquart relation / p(DM|z)) """ #standard Python imports @@ -24,7 +53,35 @@ def calc_path_priors(): """ - Loops over all ICS FRBs + Run PATH on all CRAFT ICS FRBs with and without zdm-derived priors. + + Initialises a zdm grid for the CRAFT_ICS_1300 survey and the Marnoch+2023 + host galaxy luminosity model. For each FRB in ``opt.frblist``: + + - Matches the FRB to the CRAFT_ICS_1300 survey to retrieve its + extragalactic dispersion measure (DM_EG). + - Runs PATH with a flat apparent-magnitude prior and fixed P_U=0.1 + (``usemodel=False``), giving baseline posteriors P_Ox1. + - Uses the zdm model to compute a physically motivated prior on apparent + host magnitude, p(m_r | DM_EG), via ``wrapper.init_path_raw_prior_Oi``, + and estimates P_U from the fraction of the magnitude prior that falls + below the survey detection limit via ``wrapper.estimate_unseen_prior``. + - Runs PATH again with the zdm-derived prior (``usemodel=True``) to give + updated posteriors P_Ox2. + + After processing all FRBs, produces a weighted histogram of the posterior + host apparent magnitudes (P_Ox2) across the whole sample and saves it to + ``posterior_pOx.png``. + + Notes + ----- + FRBs not found in the CRAFT_ICS_1300 survey (e.g. because they were + detected by a different instrument configuration) are skipped with a + warning. + + The zdm model parameters are held fixed at their default values; no + parameter optimisation is performed here. See + ``optimise_host_priors.py`` for the equivalent script with optimisation. """ frblist = opt.frblist diff --git a/zdm/scripts/Path/optimise_host_priors.py b/zdm/scripts/Path/optimise_host_priors.py index a1da24fb..99d9569b 100644 --- a/zdm/scripts/Path/optimise_host_priors.py +++ b/zdm/scripts/Path/optimise_host_priors.py @@ -1,19 +1,50 @@ """ -This file illustrates how to optimise the host prior -distribution by fitting to CRAFT ICS optical observations. -It fits a model of absolute galaxy magnitude distributions, -uses zDM to predict redshifts and hence apparent magntidues, -runs PATH using that prior, and tries to get priors to match posteriors. +Optimise FRB host galaxy magnitude priors using zdm predictions and PATH. -WARNING: this is NOT the optimal method! That would require using -a catalogue of galaxies to sample from to generate fake optical fields. -But nonetheless, this tests the power of estimating FRB host galaxy -contributions using zDM to set priors for apparent magnitudes. +This script fits a parametric model of FRB host galaxy absolute magnitude +distributions to the CRAFT ICS optical observations. It works by: -WARNING2: To do this properly also requires inputting the posterior POx -for host galaxies into zDM! This simulation does not do that either. +1. Initialising zdm grids for the three CRAFT ICS survey bands (892, 1300, + and 1632 MHz) using the HoffmannHalo25 parameter state. +2. Constructing a host galaxy model (``simple`` or ``loudas``) that predicts + apparent r-band magnitudes by convolving the absolute magnitude distribution + with the zdm p(z|DM_EG) redshift prior, optionally including a k-correction. +3. Running PATH with those zdm-derived apparent magnitude priors to obtain + posterior host association probabilities P_Ox for each CRAFT ICS FRB. +4. Optimising the model parameters with ``scipy.optimize.minimize`` by + minimising either a maximum-likelihood statistic or a KS-like goodness-of-fit + statistic against the observed PATH posteriors. -WARNING3: this program can take a while to run, if optimising the simple model. +After optimisation the script: + +- Saves the best-fit parameters to ``_output/best_fit_params.npy``. +- Plots the predicted vs observed apparent magnitude distributions for the + best-fit model (``best_fit_apparent_magnitudes.png``). +- Re-runs PATH with the original (flat) priors for comparison and produces a + scatter plot of best-fit vs original posteriors + (``Scatter_plot_comparison.png``). + +Limitations +----------- +- The optimal approach would sample galaxy candidates from a real photometric + catalogue to construct proper optical fields; this script uses a parametric + model instead. +- Host identification posteriors (P_Ox) are not fed back into the zdm + likelihood; a self-consistent joint fit is not performed. +- Runtime can be significant when optimising the ``simple`` model (10 free + parameters by default). + +Usage +----- +Set ``minimise = True`` (default) to run the optimiser, or ``False`` to load +previously saved parameters from ``_output/best_fit_params.npy``. +Switch between host models by changing ``modelname`` to ``"simple"`` or +``"loudas"``. + +Requirements +------------ +- ``astropath`` package (PATH implementation) +- ``frb`` package (FRB utilities and optical data) """ @@ -48,13 +79,40 @@ def main(): """ - Main function - Contains outer loop to iterate over parameters - + Optimise host galaxy magnitude model parameters and compare with baseline PATH. + + Workflow: + + 1. Load the CRAFT ICS FRB list and initialise zdm grids for the 892, 1300, + and 1632 MHz survey bands using the HoffmannHalo25 cosmological/FRB state. + 2. Select a host magnitude model (``"simple"`` or ``"loudas"``) and configure + its parameter bounds and initial values. + 3. If ``minimise=True``, call ``scipy.optimize.minimize`` with + ``on.function`` as the objective, minimising either the maximum-likelihood + statistic (``istat=1``) or the KS-like statistic (``istat=0``) over all + CRAFT ICS FRBs. Best-fit parameters are saved to + ``_output/best_fit_params.npy``. + 4. Re-evaluate PATH at the best-fit parameters and compute both the + likelihood and KS statistics; save the apparent magnitude comparison + plot to ``_output/best_fit_apparent_magnitudes.png``. + 5. Re-run PATH with the original flat priors (``usemodel=False``) and save + a scatter plot comparing original vs best-fit P_Ox posteriors to + ``_output/Scatter_plot_comparison.png``. + + Configuration knobs (edit at the top of the function body): + + - ``istat``: 0 = KS statistic, 1 = maximum-likelihood statistic. + - ``dok``: whether to include a k-correction in the apparent magnitude model. + - ``modelname``: ``"simple"`` for the parametric histogram model or + ``"loudas"`` for the Loudas single-parameter model. + - ``POxcut``: optional float (e.g. 0.9) to exclude low-confidence FRBs + from the model comparison. + - ``minimise``: set to ``False`` to skip optimisation and load saved + parameters instead. """ ######### List of all ICS FRBs for which we can run PATH ####### - # hard-coded list of FRBs with PATH data in ice paper + # hard-coded list of FRBs with PATH data in ICE paper frblist=opt.frblist # Initlisation of zDM grid @@ -176,11 +234,31 @@ def main(): plt.close() -def make_cdf_for_plotting(xvals,weights=None): +def make_cdf_for_plotting(xvals, weights=None): """ - Creates a cumulative distribution function - - xvals,yvals: values of data points + Build a step-function CDF suitable for plotting. + + Converts an array of data values (and optional weights) into paired + (x, y) arrays that trace the cumulative distribution as a staircase, + with two points per input value so that horizontal steps are rendered + correctly by matplotlib. + + Parameters + ---------- + xvals : np.ndarray + 1-D array of data values. Will be sorted in ascending order. + weights : np.ndarray, optional + 1-D array of weights with the same length as ``xvals``. If provided, + the CDF is computed as the normalised cumulative sum of the sorted + weights. If ``None``, a uniform CDF over ``N`` points is used, + with steps at ``0, 1/N, 2/N, ..., 1``. + + Returns + ------- + cx : np.ndarray + x-coordinates of the staircase CDF (length ``2 * N``). + cy : np.ndarray + y-coordinates of the staircase CDF (length ``2 * N``). """ N = xvals.size cx = np.zeros([2*N]) diff --git a/zdm/scripts/Path/plot_host_models.py b/zdm/scripts/Path/plot_host_models.py index b40d7a89..7ae403c3 100644 --- a/zdm/scripts/Path/plot_host_models.py +++ b/zdm/scripts/Path/plot_host_models.py @@ -1,16 +1,52 @@ """ -Script showing how to load different FRB host models. +Plot and compare FRB host galaxy magnitude models against CRAFT ICS PATH posteriors. -It does this for both my simple naive model, and Nick Loudas's -model based on mass- or sfr-weightings, and Lachlan's -evolution of galaxy spectra. +This script demonstrates how to load, configure, and visualise the three +available FRB host galaxy magnitude models, then evaluate PATH host association +posteriors for all CRAFT ICS FRBs using each model in turn. -We then evaluate P(O|x) for CRAFT FRBs in the CRAFT 1300 MHz survey +Model comparison +---------------- +Three host magnitude models are loaded and plotted: -It's not the simplest script, but it should show how to do a whole bunch of stuff +- **Simple model** (``opt.simple_host_model``): a parametric histogram of + absolute magnitudes M_r, linearly interpolated, with an optional + k-correction. Parameters are set by hand here (not from a fitted result). +- **Loudas model** (``opt.loudas_model``): predicts apparent magnitudes from + a galaxy luminosity function weighted by stellar mass (``fSFR=0``) or + star-formation rate (``fSFR=1``), based on Nick Loudas's galaxy model. + The mixing parameter ``fSFR`` is varied to show sensitivity. +- **Marnoch model** (``opt.marnoch_model``): predicts apparent magnitudes + from the galaxy spectral evolution model of Marnoch et al. 2023 + (MNRAS 525, 994). -NOTE: this does NOT use the best-fit distributions form the recent paper. +Diagnostic plots produced in ``Plots/`` +----------------------------------------- +- ``simple_model_mags.png``: absolute magnitude prior p(M_r) for the simple + model, showing the interpolated curve and the raw histogram bin values. +- ``loudas_model_mags.png``: apparent magnitude distributions p(m_r) for the + Loudas model at several redshifts, comparing mass- vs SFR-weighted variants. +- ``loudas_fsfr_interpolation.png``: sensitivity of the Loudas model to the + ``fSFR`` mixing parameter at z=0.5, illustrating the full allowed range. +- ``all_model_apparent_mags.png``: side-by-side comparison of p(m_r | z) for + all three models at z = 0.1, 0.5, and 2.0. +- ``all_model_mag_priors_dm.png``: PATH apparent magnitude priors p(m_r | DM) + for all three models at DM = 200, 600, and 1000 pc/cm³, using the + CRAFT_ICS_1300 zdm grid to convert DM to a redshift prior. +- ``posterior_comparison.png``: scatter plot of PATH host posteriors P(O|x) + from the original flat-prior run vs each of the four zdm-informed model + runs, across all CRAFT ICS FRBs. +Note +---- +Parameter values used here are illustrative initial estimates, not best-fit +results from the published analysis. See ``optimise_host_priors.py`` for the +fitting procedure. + +Requirements +------------ +- ``astropath`` package (PATH implementation) +- ``frb`` package (FRB utilities and optical data) """ #standard Python imports @@ -42,7 +78,46 @@ def calc_path_priors(): """ - Loops over all ICS FRBs + Generate diagnostic plots for all host models and compare PATH posteriors. + + Workflow: + + 1. **Model initialisation**: Loads the simple, Loudas (mass-weighted, + ``fSFR=0``), and Marnoch host magnitude models with illustrative + parameter values. + + 2. **Intrinsic magnitude plots**: Plots the absolute magnitude prior + p(M_r) for the simple model and apparent magnitude distributions + p(m_r) for the Loudas model at several redshifts. + + 3. **fSFR sensitivity**: Plots p(m_r | z=0.5) for the Loudas model + across a wide range of ``fSFR`` values to illustrate model behaviour + beyond the physically motivated [0, 1] range. + + 4. **Model comparison at fixed z**: Compares p(m_r | z) across all three + models at z = 0.1, 0.5, and 2.0. + + 5. **DM-dependent priors**: Loads the CRAFT_ICS_1300 zdm grid and wraps + each model in a ``model_wrapper`` to compute the PATH apparent magnitude + prior p(m_r | DM) at DM = 200, 600, and 1000 pc/cm³. + + 6. **PATH evaluation over CRAFT ICS FRBs**: For each FRB in + ``opt.frblist`` that is found in the CRAFT_ICS_1300 survey: + + - Runs PATH with a flat prior (``usemodel=False``, P_U=0.1) as the + baseline. + - Runs PATH four more times, one per zdm-informed model variant + (simple; Loudas mass-weighted; Loudas SFR-weighted; Marnoch), each + with P_U estimated from ``wrapper.estimate_unseen_prior()``. + - Prints diagnostic output for candidate host galaxies that flip above + P_Ox=0.5 relative to the baseline (simple model only). + + 7. **Posterior scatter plot**: Produces ``Plots/posterior_comparison.png`` + showing P(O|x) from each zdm-informed model against the flat-prior + baseline across all FRBs. + + Output files are written to the ``Plots/`` subdirectory, which is created + if it does not already exist. """ opdir = "Plots/" From 1b65f9a1373bbd83eecac32be4d0c31982cc38f2 Mon Sep 17 00:00:00 2001 From: "J. Xavier Prochaska" Date: Thu, 19 Mar 2026 13:03:52 +0900 Subject: [PATCH 25/35] mo docs --- docs/index.rst | 1 + docs/optical.rst | 435 +++++++++++++++++++++++++++++++++++++ zdm/optical_numerics.py | 461 ++++++++++++++++++++++++++++++---------- 3 files changed, 779 insertions(+), 118 deletions(-) create mode 100644 docs/optical.rst diff --git a/docs/index.rst b/docs/index.rst index 08c536ad..6bebe91a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -43,6 +43,7 @@ Getting Started architecture parameters api + optical .. toctree:: :maxdepth: 1 diff --git a/docs/optical.rst b/docs/optical.rst new file mode 100644 index 00000000..808897ae --- /dev/null +++ b/docs/optical.rst @@ -0,0 +1,435 @@ +.. _optical: + +================================ +Optical Host Galaxy Association +================================ + +This section describes the three modules that connect zdm's redshift-DM +predictions to the `PATH `_ (Probabilistic +Association of Transients to their Hosts) algorithm. Together they provide +physically motivated priors on FRB host galaxy apparent magnitudes, derived +from the zdm posterior p(z | DM\ :sub:`EG`). + +- :mod:`zdm.optical_params` — parameter dataclasses configuring each model +- :mod:`zdm.optical` — host magnitude models and the PATH interface wrapper +- :mod:`zdm.optical_numerics` — numerical evaluation, optimisation, and + statistics for fitting the models to CRAFT ICS optical data + +Overview +======== + +Standard PATH assigns host galaxy candidates a prior based only on galaxy +surface density and angular size. The zdm optical modules replace this with a +prior informed by p(z | DM\ :sub:`EG`): given an FRB's extragalactic DM, zdm +predicts a redshift distribution, which is convolved with a host galaxy +luminosity model to produce p(m\ :sub:`r` | DM\ :sub:`EG`). + +The modules are built around a two-layer design: + +.. code-block:: text + + ┌──────────────────────────────────────────────────────────────┐ + │ model_wrapper (optical.py) │ + │ Convolves p(m_r|z) with zdm p(z|DM_EG) → p(m_r|DM_EG) │ + │ Estimates P_U (undetected host prior) │ + │ Plugs into PATH via pathpriors.USR_raw_prior_Oi │ + └──────────────────────────────────────────────────────────────┘ + │ wraps + ┌──────────────────────────────────────────────────────────────┐ + │ Host magnitude models (optical.py) │ + │ │ + │ simple_host_model — parametric p(M_r) histogram │ + │ loudas_model — mass/SFR-weighted tables (Loudas) │ + │ marnoch_model — Gaussian fit to known hosts │ + │ (Marnoch et al. 2023) │ + └──────────────────────────────────────────────────────────────┘ + │ configured by + ┌──────────────────────────────────────────────────────────────┐ + │ Parameter dataclasses (optical_params.py) │ + │ │ + │ OpticalState ← SimpleParams, LoudasParams, │ + │ Apparent, Identification │ + └──────────────────────────────────────────────────────────────┘ + +Host Magnitude Models +===================== + +Three models are available, all implementing the same interface +``get_pmr_gz(mrbins, z)`` which returns p(m\ :sub:`r` | z) for a set of +apparent magnitude bin edges at a given redshift. + +simple_host_model +----------------- + +A parametric model describing the intrinsic absolute magnitude distribution +p(M\ :sub:`r`) as N amplitudes (default 10) at uniformly spaced points +between ``Absmin`` and ``Absmax``. The amplitudes are normalised to sum to +unity and interpolated onto a fine internal grid via one of four schemes +controlled by ``AbsModelID``: + +.. list-table:: + :header-rows: 1 + :widths: 15 85 + + * - ``AbsModelID`` + - Description + * - 0 + - Step-function histogram — each parameter value applies uniformly to + its bin + * - 1 + - Linear interpolation between parameter points *(default)* + * - 2 + - Cubic spline interpolation (negative values clamped to zero) + * - 3 + - Cubic spline in log-space (parameters are log\ :sub:`10` weights) + +Conversion from M\ :sub:`r` to m\ :sub:`r` is controlled by ``AppModelID``: + +.. list-table:: + :header-rows: 1 + :widths: 15 85 + + * - ``AppModelID`` + - Description + * - 0 + - Pure distance modulus, no k-correction *(default)* + * - 1 + - Distance modulus plus power-law k-correction + ``2.5 × k × log10(1 + z)`` + +loudas_model +------------ + +Uses precomputed p(m\ :sub:`r` | z) tables from Nick Loudas, constructed by +weighting galaxy luminosities by either stellar mass or star-formation rate. +The single free parameter ``fSFR`` interpolates between the two: + +.. math:: + + p(m_r | z) = (1 - f_{\rm SFR})\,p_{\rm mass}(m_r | z) + + f_{\rm SFR}\,p_{\rm SFR}(m_r | z) + +Interpolation between tabulated redshift bins is performed in log-z space +with a luminosity-distance shift applied to each tabulated distribution before +combining, ensuring correct apparent magnitude evolution at low redshift. + +marnoch_model +------------- + +A zero-parameter model based on Marnoch et al. 2023 (MNRAS 525, 994). Fits +a Gaussian to the r-band magnitude distribution of known CRAFT ICS FRB host +galaxies, with mean and standard deviation described as cubic splines of +redshift. No free parameters; the model is fixed by the observed host sample. + +The ``model_wrapper`` Class +=========================== + +:class:`~zdm.optical.model_wrapper` is a survey-independent wrapper around +any host model. Its key responsibilities are: + +1. **Precomputation**: at initialisation it calls ``model.get_pmr_gz`` for + every redshift value in the zdm grid to build a cached + p(m\ :sub:`r` | z) array. +2. **DM integration**: ``init_path_raw_prior_Oi(DM, grid)`` extracts + p(z | DM\ :sub:`EG`) from the grid and convolves it with the cached + array to produce p(m\ :sub:`r` | DM\ :sub:`EG`). +3. **P_U estimation**: ``estimate_unseen_prior()`` integrates the magnitude + prior against the detection probability curve + (logistic function centred on ``pU_mean`` with width ``pU_width``) to + obtain the prior probability that the true host is below the detection + limit. +4. **PATH interface**: after ``init_path_raw_prior_Oi`` is called, + ``pathpriors.USR_raw_prior_Oi`` is automatically pointed at + ``path_raw_prior_Oi``, so PATH uses the zdm-derived prior transparently. + +Typical Workflow +================ + +The following example shows how to obtain zdm-informed PATH posteriors for a +single CRAFT ICS FRB. + +.. code-block:: python + + from zdm import optical as opt + from zdm import optical_numerics as on + from zdm import loading, cosmology as cos, parameters + + # 1. Initialise zdm grid + state = parameters.State() + cos.set_cosmology(state) + cos.init_dist_measures() + ss, gs = loading.surveys_and_grids(survey_names=['CRAFT_ICS_1300']) + g, s = gs[0], ss[0] + + # 2. Choose a host magnitude model + model = opt.marnoch_model() # or simple_host_model / loudas_model + + # 3. Wrap it for the survey's redshift grid + wrapper = opt.model_wrapper(model, g.zvals) + + # 4. For a specific FRB, look up its DM_EG + frb = 'FRB20190608B' + imatch = opt.matchFRB(frb, s) + DMEG = s.DMEGs[imatch] + + # 5. Compute p(m_r | DM_EG) and estimate P_U + wrapper.init_path_raw_prior_Oi(DMEG, g) # also sets pathpriors.USR_raw_prior_Oi + PU = wrapper.estimate_unseen_prior() + + # 6. Run PATH with the zdm prior + P_O, P_Ox, P_Ux, mags, ptbl = on.run_path(frb, usemodel=True, P_U=PU) + +To process the full CRAFT ICS sample and compare models, use +:func:`~zdm.optical_numerics.calc_path_priors` directly. To fit model +parameters, pass :func:`~zdm.optical_numerics.function` as the objective to +``scipy.optimize.minimize`` — see ``zdm/scripts/Path/optimise_host_priors.py`` +for a complete example. + +Parameter Reference +=================== + +All host galaxy model parameters are held in dataclasses collected by +:class:`~zdm.optical_params.OpticalState`. The four constituent dataclasses +and their parameters are described below. + +SimpleParams +------------ + +Controls the :class:`~zdm.optical.simple_host_model`. + +.. list-table:: + :header-rows: 1 + :widths: 20 12 12 56 + + * - Parameter + - Default + - Units + - Description + * - ``Absmin`` + - −25 + - M\ :sub:`r` + - Minimum absolute magnitude of the host distribution + * - ``Absmax`` + - −15 + - M\ :sub:`r` + - Maximum absolute magnitude of the host distribution + * - ``NAbsBins`` + - 1000 + - — + - Number of internal absolute magnitude bins (fine grid for + computing p(m\ :sub:`r` | z)) + * - ``NModelBins`` + - 10 + - — + - Number of free parameter bins describing p(M\ :sub:`r`) + * - ``AbsPriorMeth`` + - 0 + - — + - Initial prior on absolute magnitudes: 0 = uniform + * - ``AbsModelID`` + - 1 + - — + - Interpolation scheme for p(M\ :sub:`r`): 0 = histogram, + 1 = linear, 2 = spline, 3 = log-spline + * - ``AppModelID`` + - 0 + - — + - Absolute-to-apparent conversion: 0 = distance modulus only, + 1 = with power-law k-correction + * - ``k`` + - 0.0 + - — + - k-correction power-law index (only used when ``AppModelID=1``) + +LoudasParams +------------ + +Controls the :class:`~zdm.optical.loudas_model`. + +.. list-table:: + :header-rows: 1 + :widths: 20 12 12 56 + + * - Parameter + - Default + - Units + - Description + * - ``fSFR`` + - 0.5 + - — + - Fraction of FRB hosts tracing star-formation rate (0 = pure + mass-weighted, 1 = pure SFR-weighted) + * - ``NzBins`` + - 10 + - — + - Number of redshift bins for histogram calculations + * - ``zmin`` + - 0.0 + - — + - Minimum redshift for p(m\ :sub:`r`) calculation + * - ``zmax`` + - 0.0 + - — + - Maximum redshift for p(m\ :sub:`r`) calculation + * - ``NMrBins`` + - 0 + - — + - Number of absolute magnitude bins + * - ``Mrmin`` + - 0.0 + - M\ :sub:`r` + - Minimum absolute magnitude + * - ``Mrmax`` + - 0.0 + - M\ :sub:`r` + - Maximum absolute magnitude + +Apparent +-------- + +Controls the apparent magnitude grid used by :class:`~zdm.optical.model_wrapper`. + +.. list-table:: + :header-rows: 1 + :widths: 20 12 12 56 + + * - Parameter + - Default + - Units + - Description + * - ``Appmin`` + - 10 + - m\ :sub:`r` + - Minimum apparent magnitude of the internal grid + * - ``Appmax`` + - 35 + - m\ :sub:`r` + - Maximum apparent magnitude of the internal grid + * - ``NAppBins`` + - 250 + - — + - Number of apparent magnitude bins + +Identification +-------------- + +Controls the survey detection completeness model used to compute P_U. +The detection probability is modelled as a logistic function: +p(detected | m\ :sub:`r`) = 1 − p(U | m\ :sub:`r`) where + +.. math:: + + p(U | m_r) = \frac{1}{1 + \exp\!\left(\frac{\mu - m_r}{w}\right)} + +with μ = ``pU_mean`` and w = ``pU_width``. + +.. list-table:: + :header-rows: 1 + :widths: 20 12 12 56 + + * - Parameter + - Default + - Units + - Description + * - ``pU_mean`` + - 26.385 + - m\ :sub:`r` + - Magnitude at which 50 % of host galaxies are undetected + (the survey's half-completeness limit). Default value is + calibrated to VLT/FORS2 R-band observations. + * - ``pU_width`` + - 0.279 + - m\ :sub:`r` + - Characteristic width of the completeness rolloff. Smaller + values give a sharper transition between detected and + undetected regimes. + +Optimisation and Statistics +============================ + +:mod:`zdm.optical_numerics` provides two goodness-of-fit statistics for +comparing the model-predicted apparent magnitude prior to observed PATH +posteriors across a sample of FRBs: + +**Maximum-likelihood statistic** (:func:`~zdm.optical_numerics.calculate_likelihood_statistic`) + +For each FRB, evaluates + +.. math:: + + \ln \mathcal{L}_i = \log_{10}\!\left(\sum_j \frac{P(O_j|x)}{s_i} + P_{U,i}^{\rm prior}\right) + +where the sum runs over candidate host galaxies and *s*\ :sub:`i` is a +rescale factor that undoes PATH's internal renormalisation. The total +statistic is Σ ln *ℒ*\ :sub:`i` over all FRBs. This is the recommended +objective for parameter fitting. + +**KS-like statistic** (:func:`~zdm.optical_numerics.calculate_ks_statistic`) + +Builds normalised cumulative distributions of the model prior and the +observed posteriors (weighted by P(O|x)) over apparent magnitude, then +returns the maximum absolute difference — analogous to the KS test statistic. +Smaller values indicate a better fit. + +Both statistics accept a ``POxcut`` argument to restrict the sample to FRBs +with a confidently identified host (max P(O|x) > threshold), simulating a +traditional host-identification approach. + +Scripts +======= + +Ready-to-run scripts using these modules are in ``zdm/scripts/Path/``: + +.. list-table:: + :header-rows: 1 + :widths: 40 60 + + * - Script + - Purpose + * - ``estimate_path_priors.py`` + - Demonstrate zdm-informed PATH priors on all CRAFT ICS FRBs; + compare flat vs. model priors; save posterior magnitude histogram + * - ``optimise_host_priors.py`` + - Fit host model parameters to the CRAFT ICS sample using + ``scipy.optimize.minimize`` + * - ``plot_host_models.py`` + - Visualise all three host models and compare their PATH posteriors + across the CRAFT ICS sample + +API Reference +============= + +optical_params +-------------- + +Parameter dataclasses for configuring host galaxy models. + +.. automodapi:: zdm.optical_params + :no-inheritance-diagram: + +optical +------- + +Host magnitude model classes and the ``model_wrapper`` PATH interface. + +.. automodapi:: zdm.optical + :no-inheritance-diagram: + +optical_numerics +---------------- + +Numerical evaluation, optimisation, and statistics for fitting host models. + +.. automodapi:: zdm.optical_numerics + :no-inheritance-diagram: + +References +========== + +- Marnoch et al. 2023, MNRAS 525, 994 — + FRB host galaxy r-band magnitude model + (https://doi.org/10.1093/mnras/stad2353) +- Macquart et al. 2020, Nature 581, 391 — + Macquart relation / p(DM | z) +- Aggarwal et al. 2021, ApJ 911, 95 — + PATH algorithm for probabilistic host association diff --git a/zdm/optical_numerics.py b/zdm/optical_numerics.py index 902ce5c5..d355a567 100644 --- a/zdm/optical_numerics.py +++ b/zdm/optical_numerics.py @@ -1,7 +1,30 @@ """ -Contains files related to numerical optimisation -of FRB host galaxy parameters. Similar to iteration.py -for the grid. +Numerical routines for evaluating and optimising FRB host galaxy magnitude models. + +This module is the numerical workhorse for the PATH integration in zdm, +analogous to ``iteration.py`` for the zdm grid. It provides: + +- **``function``** — objective function passed to ``scipy.optimize.minimize`` + that evaluates a goodness-of-fit statistic for a given set of host model + parameters against the CRAFT ICS optical data. + +- **``calc_path_priors``** — inner loop that runs PATH on a list of FRBs + across one or more surveys/grids, collecting priors, posteriors, and + undetected-host probabilities for each FRB. + +- **``run_path``** — runs the PATH algorithm for a single named FRB, + loading its candidate host galaxies from the ``frb`` package data + and applying colour corrections to convert to r-band. + +- **``calculate_likelihood_statistic``** and **``calculate_ks_statistic``** + — goodness-of-fit statistics comparing the model apparent magnitude prior + to the observed PATH posteriors across all FRBs. + +- **``make_cumulative_plots``** — plotting routine for visualising + cumulative magnitude distributions for one or more models simultaneously. + +- **``make_wrappers``**, **``make_cdf``**, **``flatten``**, + **``get_cand_properties``** — supporting utilities. """ import os @@ -19,20 +42,37 @@ def function(x,args): """ - This is a function for input into the scipi.optimize.minimise routine. - - It calculates a set of PATH priors for that model, and then calculates - a test statistic for that set. - - Args: - frblist: list of TNS FRB names - ss: list of surveys in which the FRB may exist - gs: list of grids corresponding to those surveys - model: optical model class which takes arguments x to be minimised. i.e. - the function call model.AbsPrior = x must fully specify the model. - istat [int]: which stat to use? 0 = ks stat. 1 = mak likelihood - - + Objective function for ``scipy.optimize.minimize`` over host model parameters. + + Updates the host magnitude model with parameter vector ``x``, runs PATH + on all FRBs, computes the chosen goodness-of-fit statistic, and returns + a scalar value suitable for minimisation (i.e. smaller is better). + + Parameters + ---------- + x : np.ndarray + Parameter vector passed to ``model.init_args(x)``. Its meaning + depends on the model (e.g. absolute magnitude bin weights for + ``simple_host_model``, or ``fSFR`` for ``loudas_model``). + args : list + Packed argument tuple with the following elements, in order: + + - ``frblist`` (list of str): TNS names of FRBs to evaluate. + - ``ss`` (list of Survey): surveys in which the FRBs may appear. + - ``gs`` (list of Grid): zdm grids corresponding to those surveys. + - ``model``: host magnitude model instance (must implement + ``init_args``). + - ``POxcut`` (float or None): if not None, restrict the statistic + to FRBs whose best host candidate has P(O|x) > POxcut. + - ``istat`` (int): statistic to use — 0 for KS-like statistic, + 1 for maximum-likelihood (returned as negative log-likelihood + so that minimisation maximises the likelihood). + + Returns + ------- + stat : float + Goodness-of-fit statistic (smaller is better). For ``istat=1`` + this is the negative log-likelihood. """ frblist = args[0] @@ -80,11 +120,32 @@ def make_wrappers(model,grids): return wrappers -def make_cdf(xs,ys,ws,norm = True): +def make_cdf(xs,ys,ws,norm=True): """ - makes a cumulative distribution in terms of - the x-values x, observed values y, and weights w - + Build a weighted empirical CDF evaluated on a fixed grid. + + For each grid point ``x`` in ``xs``, accumulates the weights ``ws[i]`` + of all observations ``ys[i]`` that fall below ``x``. The result is a + non-decreasing array that can be compared to a model prior CDF. + + Parameters + ---------- + xs : np.ndarray + Grid of x values at which to evaluate the CDF (e.g. apparent + magnitude bin centres). Must be sorted in ascending order. + ys : array-like + Observed data values (e.g. host galaxy apparent magnitudes). + ws : array-like + Weight for each observation in ``ys`` (e.g. PATH posteriors P_Ox). + Must have the same length as ``ys``. + norm : bool, optional + If True (default), normalise the CDF so that its maximum value + is 1. Set to False to preserve the raw cumulative weight sum. + + Returns + ------- + cdf : np.ndarray, shape (len(xs),) + Weighted empirical CDF evaluated at each point in ``xs``. """ cdf = np.zeros([xs.size]) for i,y in enumerate(ys): @@ -97,24 +158,68 @@ def make_cdf(xs,ys,ws,norm = True): def calc_path_priors(frblist,ss,gs,wrappers,verbose=True,usemodel=True,P_U=0.1): """ - Inner loop. Gets passed model parameters, but assumes everything is - initialsied from there. - - Inputs: - FRBLIST: list of FRBs to retrieve data for - ss: list of surveys modelling those FRBs (searches for FRB in data) - gs: list of zDM grids modelling those surveys - wrappers: list of optical wrapper class objects used to calculate priors on magnitude - verbose (bool): Set to true to generate further output - - Returns: - Number of FRBs fitted - AppMags: list of apparent magnitudes used internally in the model - allMagPriors: summed array of magnitude priors calculated by the model - allObsMags: list of observed magnitudes of candidate hosts - allPOx: list of posterior probabilities calculated by the model - allPU: summed values of unobserved prior - allPUx: summed values of posterior of being unobserved + Run PATH on a list of FRBs and return priors, posteriors, and P_U values. + + For each FRB in ``frblist``, searches all surveys in ``ss`` for a match, + computes the zdm-derived apparent magnitude prior (if ``usemodel=True``), + and runs PATH to produce host association posteriors. Results for all FRBs + are collected into parallel lists (one entry per FRB). + + Also writes a CSV file ``allgalaxies.csv`` (if it does not already exist) + containing the magnitude and VLT/FORS2 R-band columns for all candidate + host galaxies across all FRBs. + + Parameters + ---------- + frblist : list of str + TNS names of FRBs to process (e.g. ``['FRB20180924B', ...]``). + ss : list of Survey + Survey objects to search for each FRB. The first survey containing + a given FRB is used. + gs : list of Grid + zdm grids corresponding to each survey in ``ss``. + wrappers : list of model_wrapper + One ``model_wrapper`` per grid (from ``make_wrappers``), used to + compute DM-dependent apparent magnitude priors. + verbose : bool, optional + If True, print a warning for each FRB not found in any survey. + Defaults to True. + usemodel : bool, optional + If True, use the zdm-derived magnitude prior from ``wrappers`` and + estimate P_U from the model. If False, use PATH's built-in inverse + prior and the supplied fixed ``P_U``. Defaults to True. + P_U : float, optional + Fixed prior probability that the host galaxy is undetected. Only + used when ``usemodel=False``. Defaults to 0.1. + + Returns + ------- + nfitted : int + Number of FRBs successfully matched to a survey and processed. + AppMags : np.ndarray + Internal apparent magnitude grid (from the last processed wrapper). + allMagPriors : list of np.ndarray + One array per FRB giving p(m_r | DM_EG) on the ``AppMags`` grid. + Entries are ``None`` when ``usemodel=False``. + allObsMags : list of np.ndarray + One array per FRB listing the r-band magnitudes of PATH candidate + host galaxies. + allPO : list of np.ndarray + One array per FRB giving the PATH prior P_O for each candidate. + allPOx : list of np.ndarray + One array per FRB giving the PATH posterior P(O|x) for each candidate. + allPU : list of float + Prior P_U (probability of unseen host) for each FRB. + allPUx : list of float + Posterior P(U|x) (probability host is unseen, given data) for each FRB. + sumPU : float + Sum of ``allPU`` across all FRBs. + sumPUx : float + Sum of ``allPUx`` across all FRBs. + frbs : list of str + TNS names of the FRBs that were successfully matched and processed. + dms : list of float + Extragalactic DM (pc cm⁻³) for each FRB in ``frbs``. """ NFRB = len(frblist) @@ -243,26 +348,48 @@ def calc_path_priors(frblist,ss,gs,wrappers,verbose=True,usemodel=True,P_U=0.1): def calculate_likelihood_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUobs, PUprior,plotfile=None,POxcut=None): """ - Calculates a likelihood for each of the FRBs, and returns the log-likelihood. - - The inputs are in two categories. One is a form of lists of lists, where there is one list for - each FRB, and one entry in that list for each host galaxy candidate. Size is NFRB x NCAND - - The other input is where the length of the list matches the internal array size used to - calculate priors on host magnitudes. Size is either NMAG or NFRBxNMAG - - Inputs: - AppMags [array of floats: NMAG]: array listing apparent magnitudes used to calculate priors - AppMagPrior [array of floats NFRB xNMAG]: array giving prior on AppMags - ObsMags: list of lists of floats giving observed magnitudes m_r of host candidates - ObsPosteriors: list of lists float of posterior values P(O|x) corresponding to ObsMags - PUobs [float]: posterior on unseen probability - PUprior [float]: prior on PU - plotfile: set to name of output file for comparison plot - POxcut: if not None, cut data to fixed POx. Used to simulate current techniques - - Returns: - log likelihood of the observation + Compute the total log-likelihood of the observed PATH posteriors given the model prior. + + For each FRB, evaluates log10(Σ P(O_i|x) / rescale + P_U_prior), where the + rescale factor accounts for PATH's internal renormalisation of posteriors + relative to the model prior. Summing over all FRBs gives the total + log-likelihood returned to the caller. + + Parameters + ---------- + NFRB : int + Number of FRBs to sum over. + AppMags : np.ndarray, shape (NMAG,) + Apparent magnitude grid used to compute the model prior (not used + directly in this function, but kept for API consistency with + ``calculate_ks_statistic``). + AppMagPriors : list of np.ndarray, length NFRB + Model prior p(m_r | DM_EG) on the ``AppMags`` grid, one array per FRB. + ObsMags : list of np.ndarray, length NFRB + Observed r-band magnitudes of PATH candidate host galaxies, one array + per FRB (length NCAND varies by FRB). + ObsPosteriors : list of np.ndarray, length NFRB + PATH posterior P(O_i|x) for each candidate, one array per FRB. + PUobs : list of float, length NFRB + PATH posterior P(U|x) — probability that the true host is undetected — + for each FRB, as returned by PATH after renormalisation. + PUprior : list of float, length NFRB + Model prior P_U for each FRB, as estimated by + ``wrapper.estimate_unseen_prior()``. + plotfile : str or None, optional + If provided, save a diagnostic plot comparing prior and posterior + magnitude distributions to this file path. Defaults to None. + POxcut : float or None, optional + If not None, restrict the statistic to FRBs whose maximum P(O|x) + exceeds this threshold (simulates requiring a confident host ID). + Defaults to None. + + Returns + ------- + stat : float + Total log10-likelihood summed over all NFRB FRBs. Larger values + indicate a better fit. Multiply by -1 for use as a minimisation + objective. """ # calculates log-likelihood of observation stat=0 @@ -296,35 +423,58 @@ def calculate_likelihood_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosterio def flatten(xss): """ - Turns a list of lists into a single list + Flatten a list of lists into a single flat list. """ return [x for xs in xss for x in xs] def calculate_ks_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUobs, PUprior,plotfile=None,POxcut=None,plotlabel=None,abc=None,tag=""): """ - Calculates a ks-like statistic to be proxy for goodness-of-fit - We must set each AppMagPriors to 1.-PUprior at the limiting magnitude for each observation, - and sum the ObsPosteriors to be equal to 1.-PUobs at that magnitude. - Then these are what gets summed. - - This can be readily done by combining all ObsMags and ObsPosteriors into a single long list, - since this should already be correctly normalised. Priors require their own weight. - - Inputs: - AppMags: array listing apparent magnitudes - AppMagPriors: list of lists giving priors on AppMags for each FRB - ObsMags: list of observed magnitudes - ObsPosteriors: list of posterior values corresponding to ObsMags - PUobs: posterior on unseen probability - PUprior: prior on PU - plotfile: set to name of output file for comparison plot - POxcut: if not None, cut data to fixed POx. Used to simulate current techniques - abc [None]: add label, e.g. (a), to upper left - tag [string]: string to prefix labels - - Returns: - k-like statistic of biggest obs/prior difference + Compute a KS-like goodness-of-fit statistic between model prior and observed posteriors. + + Builds cumulative magnitude distributions for both the model prior and the + PATH posteriors, normalised by the number of FRBs, and returns the maximum + absolute difference between them — analogous to the KS test statistic. + + Optionally produces a plot comparing the two cumulative distributions. + + Parameters + ---------- + NFRB : int + Number of FRBs used for normalisation. + AppMags : np.ndarray, shape (NMAG,) + Apparent magnitude grid on which priors are defined. + AppMagPriors : list of np.ndarray, length NFRB + Model prior p(m_r | DM_EG) on ``AppMags``, one array per FRB. + ObsMags : list of np.ndarray, length NFRB + Observed r-band magnitudes of PATH candidate galaxies, one per FRB. + ObsPosteriors : list of np.ndarray, length NFRB + PATH posteriors P(O_i|x) for each candidate, one array per FRB. + PUobs : list of float + Posterior P(U|x) for each FRB (not used directly in the statistic, + kept for API consistency). + PUprior : list of float + Prior P_U for each FRB (not used directly, kept for API consistency). + plotfile : str or None, optional + If provided, save a CDF comparison plot to this path. Defaults to None. + POxcut : float or None, optional + If not None, restrict to candidates with P(O|x) > POxcut and + normalise both CDFs to unity (simulates the approach of selecting + only confidently identified hosts). Defaults to None. + plotlabel : str or None, optional + Text label placed in the centre-bottom of the plot. Defaults to None. + abc : str or None, optional + Panel label (e.g. ``'(a)'``) placed in the upper-left corner of the + figure in figure-coordinate space. Defaults to None. + tag : str, optional + String prefix added to the legend labels ``"Observed"`` and + ``"Prior"``. Defaults to ``""``. + + Returns + ------- + stat : float + Maximum absolute difference between the observed and prior cumulative + distributions. Smaller values indicate a better fit. """ # sums the apparent mag priors over all FRBs to create a cumulative distribution fAppMagPriors = np.zeros([len(AppMags)]) @@ -402,28 +552,58 @@ def make_cumulative_plots(NMODELS,NFRB,AppMags,AppMagPriors,ObsMags,ObsPosterior PUprior,plotfile,plotlabel,POxcut=None,abc=None,onlyobs=None, greyscale=[],addpriorlabel=True): """ - Creates cumulative plots of KS-like behaviour for multiple fit outcomes - - Inputs: see "calculate_ks_statistic" except: - - NMODELS (int): number of models to plot - - abc remains unchanged - - NFRB remains unchanged - - plotfile remains unchanged - - onlyobs (int): if not None, only plot observed distribution for this case - - all other parameters have a leading dimension of NMODELS - - Inputs from "calculate_ks_statistic" with extra NMODELS dimension: - AppMags: array listing apparent magnitudes - AppMagPriors: list of lists giving priors on AppMags for each FRB - ObsMags: list of observed magnitudes - ObsPosteriors: list of posterior values corresponding to ObsMags - PUobs: posterior on unseen probability - PUprior: prior on PU - POxcut: if not None, cut data to fixed POx. Used to simulate current techniques - greyscale (list of ints): if present, defines which models to plot as background greyscales - addpriorlabel (bool): if True, add "prior" to label of priors - Returns: - None + Plot cumulative apparent magnitude distributions for multiple host models on one figure. + + Computes the same normalised prior and observed CDFs as + ``calculate_ks_statistic``, but for ``NMODELS`` models simultaneously, + overlaying them on a single figure with distinct line styles. + + All list-valued parameters that appear in ``calculate_ks_statistic`` + gain an additional leading dimension of size ``NMODELS`` here. + + Parameters + ---------- + NMODELS : int + Number of models to plot. + NFRB : list of int, length NMODELS + Number of FRBs for each model, used for normalisation. + AppMags : list of np.ndarray, length NMODELS + Apparent magnitude grid for each model. + AppMagPriors : list of lists of np.ndarray, shape (NMODELS, NFRB, NMAG) + Model prior p(m_r | DM_EG) for each model and FRB. + ObsMags : list of lists of np.ndarray, shape (NMODELS, NFRB, NCAND) + Observed candidate magnitudes for each model and FRB. + ObsPosteriors : list of lists of np.ndarray, shape (NMODELS, NFRB, NCAND) + PATH posteriors P(O_i|x) for each model and FRB. + PUobs : list, length NMODELS + Posterior P(U|x) per model (not used directly in the plot). + PUprior : list, length NMODELS + Prior P_U per model (not used directly in the plot). + plotfile : str + Output file path for the saved figure. + plotlabel : list of str, length NMODELS + Legend label prefix for each model. + POxcut : float or None, optional + If not None, restrict to candidates with P(O|x) > POxcut and + normalise CDFs to unity. Defaults to None. + abc : str or None, optional + Panel label (e.g. ``'(a)'``) placed in the upper-left corner in + figure-coordinate space. Defaults to None. + onlyobs : int or None, optional + If not None, only draw the observed CDF for the model with this + index (useful when all models share the same observations). The + observed line is then labelled ``"Observed"`` without a model prefix. + Defaults to None. + greyscale : list of int, optional + Indices of models whose observed CDF should additionally be drawn + in grey (for background reference). Defaults to ``[]``. + addpriorlabel : bool, optional + If True (default), append ``": Prior"`` to each model's legend entry. + Set to False to use only ``plotlabel[imodel]`` as the label. + + Returns + ------- + None """ # arrays to hold created observed and prior distributions @@ -532,13 +712,20 @@ def make_cumulative_plots(NMODELS,NFRB,AppMags,AppMagPriors,ObsMags,ObsPosterior def get_cand_properties(frblist): """ - Returns properties of galaxy candidates for FRBs - + Load PATH candidate host galaxy properties for a list of FRBs. + + Reads the pre-generated PATH CSV files from the ``frb`` package data + directory (``frb/data/Galaxies/PATH/_PATH.csv``) and extracts + the columns ``['ang_size', 'mag', 'ra', 'dec', 'separation']`` for + each FRB. + Args: - frblist: list of strings giving FRB names - + frblist (list of str): TNS FRB names (e.g. ``['FRB20180924B', ...]``). + Returns: - all_candidates: list of pandas dataframes containing candidate info + all_candidates (list of pd.DataFrame): one DataFrame per FRB, + each with columns ``ang_size``, ``mag``, ``ra``, ``dec``, + and ``separation``. """ all_candidates=[] @@ -556,16 +743,54 @@ def get_cand_properties(frblist): all_candidates.append(candidates) return all_candidates -def run_path(name,P_U=0.1,usemodel = False, sort=False): +def run_path(name,P_U=0.1,usemodel=False,sort=False): """ - evaluates PATH on an FRB - - Args: - name [string]: TNS name of FRB - P_U [float]: unseen prior - usemodel [bool]: if True, use user-defined P_O|x model - sort [bool]: if True, sort candidates by posterior - + Run the PATH algorithm on a single FRB and return host association results. + + Loads the FRB object and its pre-generated PATH candidate table from the + ``frb`` package, applies colour corrections to convert candidate magnitudes + to r-band (using fixed offsets: I → R: +0.65, g → R: −0.65), sets up the + FRB localisation ellipse and offset prior, and evaluates PATH posteriors. + + The magnitude prior used for the candidates is: + + - ``usemodel=False``: PATH's built-in ``'inverse'`` prior (uniform in log + surface density). + - ``usemodel=True``: the ``'user'`` prior, which must be set externally by + pointing ``pathpriors.USR_raw_prior_Oi`` at a ``model_wrapper`` method + before calling this function (typically done by + ``wrapper.init_path_raw_prior_Oi``). + + The offset prior is always the ``'exp'`` model from PATH's ``'adopted'`` + standard priors, with scale 0.5 arcsec. + + Parameters + ---------- + name : str + TNS name of the FRB (e.g. ``'FRB20180924B'``). + P_U : float, optional + Prior probability that the true host galaxy is undetected. Defaults + to 0.1. + usemodel : bool, optional + If True, use the externally set user prior for candidate magnitudes. + Defaults to False. + sort : bool, optional + If True, sort the returned arrays by P(O|x) in ascending order. + Defaults to False. + + Returns + ------- + P_O : np.ndarray + Prior probability P(O_i) for each candidate host galaxy. + P_Ox : np.ndarray + Posterior probability P(O_i|x) for each candidate. + P_Ux : float + Posterior probability P(U|x) that the true host is undetected. + mags : np.ndarray + R-band apparent magnitudes of the candidates (after colour correction). + ptbl : pd.DataFrame + Full PATH candidate table loaded from the CSV file, with an + additional ``'frb'`` column set to ``name``. """ ######### Loads FRB, and modifes properties ######### From cb4b48694ac05df9bb252c5027ba6062a7ecd669 Mon Sep 17 00:00:00 2001 From: "J. Xavier Prochaska" Date: Thu, 19 Mar 2026 13:14:20 +0900 Subject: [PATCH 26/35] import fix --- docs/conf.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 3a9b2bd1..c62812ac 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -107,7 +107,13 @@ } # Mock imports for modules that may not be available during doc build +# These packages can only be installed from GitHub and are not available +# on ReadTheDocs or in the standard docs build environment: +# ne2001: pip install git+https://github.com/FRBs/ne2001.git +# frb: pip install git+https://github.com/FRBs/FRB.git +# astropath: pip install git+https://github.com/FRBs/astropath.git autodoc_mock_imports = [ 'ne2001', 'frb', + 'astropath', ] From 3d2ec48da0b5639129b26fffb016cc2a06b4dbe2 Mon Sep 17 00:00:00 2001 From: Clancy James Date: Thu, 19 Mar 2026 14:57:04 +0800 Subject: [PATCH 27/35] Updated according to Michele's latest P(O|m) data --- papers/pathpriors/compare_posteriors.py | 3 +-- papers/pathpriors/fit_loudas_model.py | 1 - .../pO_g_mr/pu_mr_vs_mag_CRAFT_VLT_FORS2_r.csv | 12 ++++++------ papers/pathpriors/plot_marnoch_model.py | 1 - papers/pathpriors/simple_systematics.py | 2 +- zdm/optical_params.py | 4 ++-- 6 files changed, 10 insertions(+), 13 deletions(-) diff --git a/papers/pathpriors/compare_posteriors.py b/papers/pathpriors/compare_posteriors.py index 9fc7c8b7..133eac7e 100644 --- a/papers/pathpriors/compare_posteriors.py +++ b/papers/pathpriors/compare_posteriors.py @@ -49,7 +49,6 @@ def main(): ######### List of all ICS FRBs for which we can run PATH ####### # hard-coded list of FRBs with PATH data in ice paper frblist=opt.frblist - frblist.remove('FRB20230731A') # too reddened # Initlisation of zDM grid # Eventually, this should be part of the loop, i.e. host IDs should @@ -234,7 +233,7 @@ def main(): continue - string2 = f"{all_candidates[i]['ra'][j]:.4f} & ${all_candidates[i]['dec'][j]:.4f}$ &" + string2 = f"{all_candidates[i]['ra'][j]:.4f} & {all_candidates[i]['dec'][j]:.4f} &" string2 += f" {all_candidates[i]['separation'][j]:.2f} &" string2 += f" {all_candidates[i]['ang_size'][j]:.2f} & {all_candidates[i]['mag'][j]:.2f} &" diff --git a/papers/pathpriors/fit_loudas_model.py b/papers/pathpriors/fit_loudas_model.py index d498e11c..bb0e3b0b 100644 --- a/papers/pathpriors/fit_loudas_model.py +++ b/papers/pathpriors/fit_loudas_model.py @@ -43,7 +43,6 @@ def main(): ######### List of all ICS FRBs for which we can run PATH ####### # hard-coded list of FRBs with PATH data in ice paper frblist=opt.frblist - frblist.remove('FRB20230731A') # too reddened # Initlisation of zDM grid # Eventually, this should be part of the loop, i.e. host IDs should diff --git a/papers/pathpriors/pO_g_mr/pu_mr_vs_mag_CRAFT_VLT_FORS2_r.csv b/papers/pathpriors/pO_g_mr/pu_mr_vs_mag_CRAFT_VLT_FORS2_r.csv index de340cd4..3c354326 100644 --- a/papers/pathpriors/pO_g_mr/pu_mr_vs_mag_CRAFT_VLT_FORS2_r.csv +++ b/papers/pathpriors/pO_g_mr/pu_mr_vs_mag_CRAFT_VLT_FORS2_r.csv @@ -17,12 +17,12 @@ mag,PU_mr 23.75,0.0 24.25,0.0 24.75,0.0 -25.25,0.0 -25.75,0.07053158399229198 -26.25,0.40295047266822037 -26.75,0.7659624379851182 -27.25,0.9680433810752437 -27.75,0.9999032120695396 +25.25,0.048275021085262715 +25.75,0.24650222799336496 +26.25,0.5408575679924887 +26.75,0.8386567720731101 +27.25,0.9805334874938698 +27.75,0.9998839106072703 28.25,1.0 28.75,1.0 29.25,1.0 diff --git a/papers/pathpriors/plot_marnoch_model.py b/papers/pathpriors/plot_marnoch_model.py index 27cd8540..36ac8108 100644 --- a/papers/pathpriors/plot_marnoch_model.py +++ b/papers/pathpriors/plot_marnoch_model.py @@ -42,7 +42,6 @@ def main(): ######### List of all ICS FRBs for which we can run PATH ####### # hard-coded list of FRBs with PATH data in ice paper frblist=opt.frblist - frblist.remove('FRB20230731A') # too reddened # Initlisation of zDM grid # Eventually, this should be part of the loop, i.e. host IDs should diff --git a/papers/pathpriors/simple_systematics.py b/papers/pathpriors/simple_systematics.py index 5de6f412..14991f46 100644 --- a/papers/pathpriors/simple_systematics.py +++ b/papers/pathpriors/simple_systematics.py @@ -75,7 +75,7 @@ def main(): if not os.path.exists(opdir): os.mkdir(opdir) - load = False + load = True colours = ["grey","orange","blue"] markers = ['o','x','s'] diff --git a/zdm/optical_params.py b/zdm/optical_params.py index 31151b34..b9a1a4df 100644 --- a/zdm/optical_params.py +++ b/zdm/optical_params.py @@ -130,13 +130,13 @@ class Identification(data_class.myDataClass): # parameters for identifying galaxies in an image """ pU_mean: float = field( - default=26.385, + default=26.176, metadata={'help': "Magnitude at which pU|mr is 0.5", 'unit': '', 'Notation': '', }) pU_width: float = field( - default=0.279, + default=0.342, metadata={'help': "Width of pU|mr distribution in ln space", 'unit': '', 'Notation': '', From fe26a838df4cdd6843086dde4bb2103a0b1d018c Mon Sep 17 00:00:00 2001 From: Clancy James Date: Thu, 19 Mar 2026 15:32:29 +0800 Subject: [PATCH 28/35] removed pkg resources --- zdm/tests/test_energetics.py | 2 -- zdm/tests/test_scat_methods.py | 6 ++---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/zdm/tests/test_energetics.py b/zdm/tests/test_energetics.py index 40256a2b..7bf39ccf 100644 --- a/zdm/tests/test_energetics.py +++ b/zdm/tests/test_energetics.py @@ -21,5 +21,3 @@ def test_init_gamma(): assert np.isclose(float(energetics.igamma_linear_log10[-1](0.)), float(energetics.igamma_linear[-1](1.)), rtol=1e-3) - -test_init_gamma() diff --git a/zdm/tests/test_scat_methods.py b/zdm/tests/test_scat_methods.py index 7c3471d1..be238f6e 100644 --- a/zdm/tests/test_scat_methods.py +++ b/zdm/tests/test_scat_methods.py @@ -1,6 +1,6 @@ #import pytest -from pkg_resources import resource_filename +import importlib.resources as resources import os import pytest #import copy @@ -46,7 +46,7 @@ def test_scat_methods(): # Initialise survey and grid # For this purporse, we only need two different surveys - sdir = os.path.join(resource_filename('zdm', 'data'), 'Surveys') + sdir = resources.files('zdm').joinpath('data/Surveys') name = 'CRAFT/ICS892' s1,g1 = loading.surveys_and_grids( state_dict=vparam_dict1, @@ -149,5 +149,3 @@ def test_scat_methods(): plt.tight_layout() plt.savefig(opdir+'/model_comparison.pdf') plt.close() - -test_scat_methods() From 268fe9f57a1c8bcddd3744feac4613d107a46c8d Mon Sep 17 00:00:00 2001 From: "J. Xavier Prochaska" Date: Thu, 19 Mar 2026 16:50:01 +0900 Subject: [PATCH 29/35] mo --- .github/workflows/ci_tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml index 66973e06..229c112c 100644 --- a/.github/workflows/ci_tests.yml +++ b/.github/workflows/ci_tests.yml @@ -33,6 +33,7 @@ jobs: python -m pip install wheel scipy IPython astropy extension-helpers mpmath python -m pip install git+https://github.com/FRBs/ne2001.git#egg=ne2001 python -m pip install git+https://github.com/FRBs/FRB.git#egg=frb + python -m pip install git+https://github.com/FRBs/astropath.git#egg=astropath - name: Test with tox run: | tox -e ${{ matrix.toxenv }} From 6e901a7837d6a0f31f0e33d3e3a025dd5ec4f531 Mon Sep 17 00:00:00 2001 From: "J. Xavier Prochaska" Date: Thu, 19 Mar 2026 16:51:49 +0900 Subject: [PATCH 30/35] another try --- docs/conf.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index c62812ac..447b709e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -5,10 +5,33 @@ import os import sys +from unittest.mock import MagicMock # Add the project root to the path for autodoc sys.path.insert(0, os.path.abspath('..')) +# Inject mocks for GitHub-only packages directly into sys.modules. +# This must be done before Sphinx loads any extensions, because +# sphinx-automodapi's automodsumm handler fires at builder-inited — +# earlier than autodoc_mock_imports takes effect — and will fail with +# "No module named X" if the package is not installed. +# +# Packages mocked here: +# astropath: pip install git+https://github.com/FRBs/astropath.git +# frb: pip install git+https://github.com/FRBs/FRB.git +# ne2001: pip install git+https://github.com/FRBs/ne2001.git +_MOCK_MODULES = [ + 'astropath', + 'astropath.priors', + 'astropath.path', + 'frb', + 'frb.frb', + 'frb.associate', + 'ne2001', +] +for _mod in _MOCK_MODULES: + sys.modules[_mod] = MagicMock() + # -- Project information ----------------------------------------------------- project = 'zdm' copyright = '2024, Clancy James and contributors' From 355ec4681375783c87f273621b1e66afadf3b76b Mon Sep 17 00:00:00 2001 From: "J. Xavier Prochaska" Date: Thu, 19 Mar 2026 16:54:11 +0900 Subject: [PATCH 31/35] mo --- docs/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/conf.py b/docs/conf.py index 447b709e..3f2865a2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -26,6 +26,7 @@ 'astropath.path', 'frb', 'frb.frb', + 'frb.dm', 'frb.associate', 'ne2001', ] From 80ee984e4e1e28af1166be16ebc9ec123e8eefea Mon Sep 17 00:00:00 2001 From: Clancy James Date: Fri, 20 Mar 2026 07:10:41 +0800 Subject: [PATCH 32/35] debugging still --- setup.cfg | 1 + zdm/optical.py | 4 ++-- zdm/optical_numerics.py | 10 +++++----- zdm/tests/test_path_prior.py | 22 +++++++++++++++------- zdm/tests/test_scat_methods.py | 2 ++ 5 files changed, 25 insertions(+), 14 deletions(-) diff --git a/setup.cfg b/setup.cfg index b8270df0..712df842 100644 --- a/setup.cfg +++ b/setup.cfg @@ -48,6 +48,7 @@ install_requires = cmasher>=1.9 ne2001 @ git+https://github.com/FRBs/ne2001 frb @ git+https://github.com/FRBs/FRB + astropath @ git+https://github.com/FRBs/astropath [options.extras_require] test = diff --git a/zdm/optical.py b/zdm/optical.py index eaa398c1..f62b527e 100644 --- a/zdm/optical.py +++ b/zdm/optical.py @@ -10,7 +10,7 @@ ------------ The module is built around a two-layer design: -**Host magnitude models** — each describes the intrinsic absolute +**Host magnitude models** each describes the intrinsic absolute magnitude distribution of FRB host galaxies, p(M_r), and can convert it to an apparent magnitude distribution p(m_r | z) at a given redshift. Three models are provided: @@ -31,7 +31,7 @@ Marnoch et al. 2023 (MNRAS 525, 994), using cubic splines for the redshift-dependent mean and standard deviation. -**``model_wrapper``** — a survey-independent wrapper around any host +**``model_wrapper``** a survey-independent wrapper around any host model. Given a zdm ``Grid`` and an observed DM_EG, it convolves p(m_r | z) with the zdm p(z | DM_EG) posterior to produce a DM-informed apparent magnitude prior for PATH. It also estimates diff --git a/zdm/optical_numerics.py b/zdm/optical_numerics.py index d355a567..688aa179 100644 --- a/zdm/optical_numerics.py +++ b/zdm/optical_numerics.py @@ -4,15 +4,15 @@ This module is the numerical workhorse for the PATH integration in zdm, analogous to ``iteration.py`` for the zdm grid. It provides: -- **``function``** — objective function passed to ``scipy.optimize.minimize`` +- **``function``** objective function passed to ``scipy.optimize.minimize`` that evaluates a goodness-of-fit statistic for a given set of host model parameters against the CRAFT ICS optical data. -- **``calc_path_priors``** — inner loop that runs PATH on a list of FRBs +- **``calc_path_priors``** inner loop that runs PATH on a list of FRBs across one or more surveys/grids, collecting priors, posteriors, and undetected-host probabilities for each FRB. -- **``run_path``** — runs the PATH algorithm for a single named FRB, +- **``run_path``** runs the PATH algorithm for a single named FRB, loading its candidate host galaxies from the ``frb`` package data and applying colour corrections to convert to r-band. @@ -20,11 +20,11 @@ — goodness-of-fit statistics comparing the model apparent magnitude prior to the observed PATH posteriors across all FRBs. -- **``make_cumulative_plots``** — plotting routine for visualising +- **``make_cumulative_plots``** plotting routine for visualising cumulative magnitude distributions for one or more models simultaneously. - **``make_wrappers``**, **``make_cdf``**, **``flatten``**, - **``get_cand_properties``** — supporting utilities. + **``get_cand_properties``** supporting utilities. """ import os diff --git a/zdm/tests/test_path_prior.py b/zdm/tests/test_path_prior.py index b5edcadb..52e5ba1d 100644 --- a/zdm/tests/test_path_prior.py +++ b/zdm/tests/test_path_prior.py @@ -30,15 +30,23 @@ def test_path_priors(): state = parameters.State() cos.set_cosmology(state) cos.init_dist_measures() - model = opt.host_model() + model1 = opt.simple_host_model() + model2 = opt.marnoch_model() + model3 = opt.loudas_model() name='CRAFT_ICS_1300' ss,gs = loading.surveys_and_grids(survey_names=[name]) g = gs[0] s = ss[0] - # must be done once for any fixed zvals - model.init_zmapping(g.zvals) + # wrapper around the optical model. For returning p(m_r|DM) + wrapper1 = opt.model_wrapper(model1,g.zvals) # simple + wrapper2 = opt.model_wrapper(model2,g.zvals) # loudas with fsfr=0 + wrapper3 = opt.model_wrapper(model3,g.zvals) # loudas with fsfr=0 + # must be done once for any fixed zvals + wrapper1.init_zmapping(g.zvals) + wrapper2.init_zmapping(g.zvals) + wrapper3.init_zmapping(g.zvals) for frb in frblist: # interates over the FRBs. "Do FRB" @@ -56,8 +64,8 @@ def test_path_priors(): DMEG = s.DMEGs[imatch] - prior = model.init_path_raw_prior_Oi(DMEG,g) - PU = model.estimate_unseen_prior(mag_limit=26) # might not be correct + wrapper1.init_path_raw_prior_Oi(DMEG,g) + PU = wrapper1.estimate_unseen_prior() # might not be correct # the model should have calculated a valid unseen probability if PU < 0. or PU > 1.: @@ -66,11 +74,11 @@ def test_path_priors(): if not np.isfinite(PU): raise ValueError("Unseen probability PU is ",PU) - bad = np.where(prior < 0.)[0] + bad = np.where(wrapper1.priors < 0.)[0] if len(bad) > 0: raise ValueError("Some elements of model prior have negative probability") - OK = np.all(np.isfinite(prior)) + OK = np.all(np.isfinite(wrapper1.priors)) if not OK: raise ValueError("Some elements of magnitude priors are not finite") diff --git a/zdm/tests/test_scat_methods.py b/zdm/tests/test_scat_methods.py index be238f6e..c25fff55 100644 --- a/zdm/tests/test_scat_methods.py +++ b/zdm/tests/test_scat_methods.py @@ -149,3 +149,5 @@ def test_scat_methods(): plt.tight_layout() plt.savefig(opdir+'/model_comparison.pdf') plt.close() + +test_scat_methods() From 264d561f4f637a6449f7640a0f3b8979de6f4028 Mon Sep 17 00:00:00 2001 From: Clancy James Date: Fri, 20 Mar 2026 07:32:02 +0800 Subject: [PATCH 33/35] minor change --- zdm/optical.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/zdm/optical.py b/zdm/optical.py index f62b527e..f99547a8 100644 --- a/zdm/optical.py +++ b/zdm/optical.py @@ -68,7 +68,8 @@ import pandas import h5py -import astropath.priors as pathpriors +from astropath import priors as pathpriors +#import astropath.priors as pathpriors ################################################################### From ddc5c0763d58c4a7c9b2bf13ca01233ef802ddcf Mon Sep 17 00:00:00 2001 From: Clancy James Date: Fri, 20 Mar 2026 08:55:35 +0800 Subject: [PATCH 34/35] changed where frb lists sits --- papers/pathpriors/compare_posteriors.py | 3 ++- papers/pathpriors/fit_loudas_model.py | 3 ++- papers/pathpriors/fit_simple_model.py | 3 ++- papers/pathpriors/plot_craft_optical_data.py | 3 ++- papers/pathpriors/plot_host_models.py | 3 ++- papers/pathpriors/plot_marnoch_model.py | 3 ++- papers/pathpriors/simple_systematics.py | 3 ++- zdm/frb_lists.py | 19 +++++++++++++++++++ zdm/optical.py | 18 ------------------ zdm/optical_params.py | 4 +++- zdm/scripts/Path/estimate_path_priors.py | 5 +++-- zdm/scripts/Path/optimise_host_priors.py | 3 ++- zdm/scripts/Path/plot_host_models.py | 5 +++-- 13 files changed, 44 insertions(+), 31 deletions(-) create mode 100644 zdm/frb_lists.py diff --git a/papers/pathpriors/compare_posteriors.py b/papers/pathpriors/compare_posteriors.py index 133eac7e..149ac29b 100644 --- a/papers/pathpriors/compare_posteriors.py +++ b/papers/pathpriors/compare_posteriors.py @@ -24,6 +24,7 @@ from zdm import loading from zdm import optical_numerics as on from zdm import states +from zdm import frb_lists as lists # other FRB library imports import astropath.priors as pathpriors @@ -48,7 +49,7 @@ def main(): ######### List of all ICS FRBs for which we can run PATH ####### # hard-coded list of FRBs with PATH data in ice paper - frblist=opt.frblist + frblist = lists.icslist # Initlisation of zDM grid # Eventually, this should be part of the loop, i.e. host IDs should diff --git a/papers/pathpriors/fit_loudas_model.py b/papers/pathpriors/fit_loudas_model.py index bb0e3b0b..79a9a30f 100644 --- a/papers/pathpriors/fit_loudas_model.py +++ b/papers/pathpriors/fit_loudas_model.py @@ -19,6 +19,7 @@ from zdm import loading from zdm import optical_numerics as on from zdm import states +from zdm import frb_lists as lists # other FRB library imports import astropath.priors as pathpriors @@ -42,7 +43,7 @@ def main(): ######### List of all ICS FRBs for which we can run PATH ####### # hard-coded list of FRBs with PATH data in ice paper - frblist=opt.frblist + frblist = lists.icslist # Initlisation of zDM grid # Eventually, this should be part of the loop, i.e. host IDs should diff --git a/papers/pathpriors/fit_simple_model.py b/papers/pathpriors/fit_simple_model.py index c4c37c8e..953b439d 100644 --- a/papers/pathpriors/fit_simple_model.py +++ b/papers/pathpriors/fit_simple_model.py @@ -23,6 +23,7 @@ from zdm import loading from zdm import optical_numerics as on from zdm import states +from zdm import frb_lists as lists # other FRB library imports import astropath.priors as pathpriors @@ -46,7 +47,7 @@ def main(): ######### List of all ICS FRBs for which we can run PATH ####### # hard-coded list of FRBs with PATH data in ice paper - frblist=opt.frblist + frblist = lists.icslist # Initlisation of zDM grid # Eventually, this should be part of the loop, i.e. host IDs should diff --git a/papers/pathpriors/plot_craft_optical_data.py b/papers/pathpriors/plot_craft_optical_data.py index e18b1047..dc21e0d3 100644 --- a/papers/pathpriors/plot_craft_optical_data.py +++ b/papers/pathpriors/plot_craft_optical_data.py @@ -14,6 +14,7 @@ from scipy.integrate import quad # imports from the "FRB" series +from zdm import frb_lists as lists from zdm import optical as opt from frb.frb import FRB from astropath import chance @@ -37,7 +38,7 @@ def main(): """ # gets this - frblist = opt.frblist + frblist = lists.icslist maglist=[] anglist=[] diff --git a/papers/pathpriors/plot_host_models.py b/papers/pathpriors/plot_host_models.py index 23ec7d17..6ccbfe00 100644 --- a/papers/pathpriors/plot_host_models.py +++ b/papers/pathpriors/plot_host_models.py @@ -16,6 +16,7 @@ from zdm import cosmology as cos from zdm import parameters from zdm import loading +from zdm import frb_lists as lists import astropath.priors as pathpriors @@ -40,7 +41,7 @@ def calc_path_priors(): ######## initialises optical-independent info ######## #frblist is a hard-coded list of FRBs for which we have optical PATH data - frblist = opt.frblist + frblist = lists.icslist NFRB = len(frblist) diff --git a/papers/pathpriors/plot_marnoch_model.py b/papers/pathpriors/plot_marnoch_model.py index 36ac8108..d4d1ecb9 100644 --- a/papers/pathpriors/plot_marnoch_model.py +++ b/papers/pathpriors/plot_marnoch_model.py @@ -18,6 +18,7 @@ from zdm import loading from zdm import optical_numerics as on from zdm import states +from zdm import frb_lists as lists # other FRB library imports import astropath.priors as pathpriors @@ -41,7 +42,7 @@ def main(): ######### List of all ICS FRBs for which we can run PATH ####### # hard-coded list of FRBs with PATH data in ice paper - frblist=opt.frblist + frblist = lists.icslist # Initlisation of zDM grid # Eventually, this should be part of the loop, i.e. host IDs should diff --git a/papers/pathpriors/simple_systematics.py b/papers/pathpriors/simple_systematics.py index 14991f46..f68e9daf 100644 --- a/papers/pathpriors/simple_systematics.py +++ b/papers/pathpriors/simple_systematics.py @@ -20,6 +20,7 @@ from zdm import loading from zdm import optical_numerics as on from zdm import states +from zdm import frb_lists as lists # other FRB library imports import astropath.priors as pathpriors @@ -44,7 +45,7 @@ def main(): ######### List of all ICS FRBs for which we can run PATH ####### # hard-coded list of FRBs with PATH data in ice paper - frblist=opt.frblist + frblist = lists.icslist # Initlisation of zDM grid # Eventually, this should be part of the loop, i.e. host IDs should diff --git a/zdm/frb_lists.py b/zdm/frb_lists.py new file mode 100644 index 00000000..a9b2d033 --- /dev/null +++ b/zdm/frb_lists.py @@ -0,0 +1,19 @@ +""" +This file holds lists of localsied FRBs for which optical info is +available, and can thus be used in analyses. +""" + +# this defines the ICS FRBs for which we have PATH info +# from Shannon et al (ICS sample) +# notes: FRB20230731A and 'FRB20230718A' are too reddened, so are removed +# still aiming to follow up frb20240208A and frb20240318A +icslist=['FRB20180924B','FRB20181112A','FRB20190102C','FRB20190608B', + 'FRB20190611B','FRB20190711A','FRB20190714A','FRB20191001A', + 'FRB20191228A','FRB20200430A','FRB20200906A','FRB20210117A', + 'FRB20210320C','FRB20210807D','FRB20210912A','FRB20211127I','FRB20211203C', + 'FRB20211212A','FRB20220105A','FRB20220501C', + 'FRB20220610A','FRB20220725A','FRB20220918A', + 'FRB20221106A','FRB20230526A','FRB20230708A', + 'FRB20230902A','FRB20231226A','FRB20240201A', + 'FRB20240210A','FRB20240304A','FRB20240310A'] + diff --git a/zdm/optical.py b/zdm/optical.py index f99547a8..5b1bbd01 100644 --- a/zdm/optical.py +++ b/zdm/optical.py @@ -1603,22 +1603,6 @@ def matchFRB(TNSname,survey): return match -# this defines the ICS FRBs for which we have PATH info -# notes: FRB20230731A and 'FRB20230718A' are too reddened, so are removed -# still aiming to follow up frb20240208A and frb20240318A -frblist=['FRB20180924B','FRB20181112A','FRB20190102C','FRB20190608B', - 'FRB20190611B','FRB20190711A','FRB20190714A','FRB20191001A', - 'FRB20191228A','FRB20200430A','FRB20200906A','FRB20210117A', - 'FRB20210320C','FRB20210807D','FRB20210912A','FRB20211127I','FRB20211203C', - 'FRB20211212A','FRB20220105A','FRB20220501C', - 'FRB20220610A','FRB20220725A','FRB20220918A', - 'FRB20221106A','FRB20230526A','FRB20230708A', - 'FRB20230902A','FRB20231226A','FRB20240201A', - 'FRB20240210A','FRB20240304A','FRB20240310A'] - - - - def plot_frb(name,ralist,declist,plist,opfile): """ Plot an FRB localisation and its PATH host galaxy candidates. @@ -1691,8 +1675,6 @@ def plot_frb(name,ralist,declist,plist,opfile): plt.tight_layout() plt.close() - - def pUgm(mag,mean,width): """ Return the probability that a galaxy is undetected as a function of magnitude. diff --git a/zdm/optical_params.py b/zdm/optical_params.py index b9a1a4df..ace06943 100644 --- a/zdm/optical_params.py +++ b/zdm/optical_params.py @@ -127,7 +127,9 @@ class LoudasParams(data_class.myDataClass): @dataclass class Identification(data_class.myDataClass): """ - # parameters for identifying galaxies in an image + Data class for holding parameters related to identifying galaxies in an image. + These describe the mean and deviation for the function pUgm in optical.py + that parameterises p(U|mr) """ pU_mean: float = field( default=26.176, diff --git a/zdm/scripts/Path/estimate_path_priors.py b/zdm/scripts/Path/estimate_path_priors.py index b3b1155e..f4c81bf6 100644 --- a/zdm/scripts/Path/estimate_path_priors.py +++ b/zdm/scripts/Path/estimate_path_priors.py @@ -47,6 +47,7 @@ from zdm import cosmology as cos from zdm import parameters from zdm import loading +from zdm import frb_lists as lists import astropath.priors as pathpriors @@ -56,7 +57,7 @@ def calc_path_priors(): Run PATH on all CRAFT ICS FRBs with and without zdm-derived priors. Initialises a zdm grid for the CRAFT_ICS_1300 survey and the Marnoch+2023 - host galaxy luminosity model. For each FRB in ``opt.frblist``: + host galaxy luminosity model. For each FRB in ``frblist.icslist``: - Matches the FRB to the CRAFT_ICS_1300 survey to retrieve its extragalactic dispersion measure (DM_EG). @@ -84,7 +85,7 @@ def calc_path_priors(): ``optimise_host_priors.py`` for the equivalent script with optimisation. """ - frblist = opt.frblist + frblist = lists.icslist NFRB = len(frblist) diff --git a/zdm/scripts/Path/optimise_host_priors.py b/zdm/scripts/Path/optimise_host_priors.py index 99d9569b..bc393f3b 100644 --- a/zdm/scripts/Path/optimise_host_priors.py +++ b/zdm/scripts/Path/optimise_host_priors.py @@ -63,6 +63,7 @@ from zdm import loading from zdm import optical_numerics as on from zdm import states +from zdm import frb_lists as lists # other FRB library imports import astropath.priors as pathpriors @@ -113,7 +114,7 @@ def main(): ######### List of all ICS FRBs for which we can run PATH ####### # hard-coded list of FRBs with PATH data in ICE paper - frblist=opt.frblist + frblist = lists.icslist # Initlisation of zDM grid # Eventually, this should be part of the loop, i.e. host IDs should diff --git a/zdm/scripts/Path/plot_host_models.py b/zdm/scripts/Path/plot_host_models.py index 7ae403c3..eab95db5 100644 --- a/zdm/scripts/Path/plot_host_models.py +++ b/zdm/scripts/Path/plot_host_models.py @@ -62,6 +62,7 @@ from zdm import cosmology as cos from zdm import parameters from zdm import loading +from zdm import frb_lists as lists import astropath.priors as pathpriors @@ -102,7 +103,7 @@ def calc_path_priors(): prior p(m_r | DM) at DM = 200, 600, and 1000 pc/cm³. 6. **PATH evaluation over CRAFT ICS FRBs**: For each FRB in - ``opt.frblist`` that is found in the CRAFT_ICS_1300 survey: + ``data/optical/frb_lists.icslist`` that is found in the CRAFT_ICS_1300 survey: - Runs PATH with a flat prior (``usemodel=False``, P_U=0.1) as the baseline. @@ -135,7 +136,7 @@ def calc_path_priors(): ######## initialises optical-independent info ######## #frblist is a hard-coded list of FRBs for which we have optical PATH data - frblist = opt.frblist + frblist = lists.icslist NFRB = len(frblist) From e7767ccd3634bdf0ca2b748086e82690a39742fa Mon Sep 17 00:00:00 2001 From: Clancy James Date: Fri, 20 Mar 2026 09:24:21 +0800 Subject: [PATCH 35/35] described zfraction --- zdm/grid.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/zdm/grid.py b/zdm/grid.py index dea68aac..5e940546 100644 --- a/zdm/grid.py +++ b/zdm/grid.py @@ -540,6 +540,10 @@ def calc_rates(self): print("WARNING: no volumetric probability pdv yet calculated") exit() + # zfraction describes the fraction of host galaxies estimated to be + # visible at a given redshift. Implementing zfraction then means this grid + # is calculating the *observable* z-DM space, rather than the intrinsic z-DM space + # zfractions are given as two arrays - the zvalues, and the f(z) values if self.survey.survey_data.observing.Z_FRACTION is not None: fdir = str(resources.files('zdm').joinpath('data/optical')) ffile = fdir + "/fz_"+str(self.survey.survey_data.observing.Z_FRACTION)+".npy"