Galaxy selection methods

As is well known, any photometric study requires some sample of galaxy candidates selected from the photometric catalogue. In this notebook, we will use a myriad of examples to showcase the available selection methods in galfind. The future aim of this code is to implement plug-in architecture here so that the user can provide and use additional selection functions that will be recognized by the galfind Catalogue class.

Let’s first create a Galaxy object for the highest redshift photometric galaxy candidate in JOF identified by Robertson et al. 2023 at z=14.63, which has NIRCam ID=718 in our galfind produced catalogue.

[1]:
# imports
import astropy.units as u
from copy import deepcopy
from galfind import Catalogue, Multiple_Selector
from galfind.Data import morgan_version_to_dir
WARNING:galfind:Could not change permissions of /raid/scratch/work/austind/GALFIND_WORK/Log_files/2024-12-11.log to 777.
Reading GALFIND config file from: /nvme/scratch/work/austind/GALFIND/galfind/../configs/galfind_config.ini
WARNING:galfind:Aperture corrections for VISTA not found in /nvme/scratch/work/austind/GALFIND/galfind/Aperture_corrections/VISTA_aper_corr.txt
WARNING:galfind:Aperture corrections for VISTA not found in /nvme/scratch/work/austind/GALFIND/galfind/Aperture_corrections/VISTA_aper_corr.txt
Failed to `import dust_attenuation`
Install from the repo with $ pip install git+https://github.com/karllark/dust_attenuation.git
[2]:
survey = "JOF"
version = "v11"
instrument_names = ["NIRCam"]
aper_diams = [0.32] * u.arcsec
forced_phot_band = ["F277W", "F356W", "F444W"]
min_flux_pc_err = 10.

cat = Catalogue.pipeline(
    survey,
    version,
    instrument_names = instrument_names,
    version_to_dir_dict = morgan_version_to_dir,
    aper_diams = aper_diams,
    forced_phot_band = forced_phot_band,
    min_flux_pc_err = min_flux_pc_err
)
# TODO: Smooth galaxy load-in
# from Robertson et al. 2023
gal = cat[717]
print(gal)
INFO:galfind:Loaded aper_diams=<Quantity [0.32] arcsec> for F277W+F356W+F444W
INFO:galfind:Combined mask for NIRCam/F277W+F356W+F444W already exists at /raid/scratch/work/austind/GALFIND_WORK/Masks/JOF/combined/JOF_F277W+F356W+F444W_auto.fits
WARNING: hdu= was not specified but multiple tables are present, reading in first available table (hdu=1) [astropy.io.fits.connect]
WARNING:astroquery:hdu= was not specified but multiple tables are present, reading in first available table (hdu=1)
WARNING:galfind:Aperture correction columns already in /raid/scratch/work/austind/GALFIND_WORK/Catalogues/v11/NIRCam/JOF/(0.32)as/JOF_MASTER_Sel-F277W+F356W+F444W_v11.fits
Calculating depths:   0%|          | 0/15 [00:00<?, ?it/s]
INFO:galfind:Calculated/loaded depths for JOF v11 NIRCam
INFO:galfind:Local depth columns already exist in /raid/scratch/work/austind/GALFIND_WORK/Catalogues/v11/NIRCam/JOF/(0.32)as/JOF_MASTER_Sel-F277W+F356W+F444W_v11.fits
INFO:galfind:Loaded 'has_data_mask' from /raid/scratch/work/austind/GALFIND_WORK/Masks/JOF/has_data_mask/JOF_MASTER_Sel-F277W+F356W+F444W_v11.h5
INFO:galfind:Making JOF v11 JOF_MASTER_Sel-F277W+F356W+F444W_v11 catalogue!
WARNING:galfind:cat_aper_diams not in kwargs.keys()=dict_keys(['ZP', 'min_flux_pc_err'])! Setting to aper_diams=<Quantity [0.32] arcsec>
WARNING:galfind:cat_aper_diams not in kwargs.keys()=dict_keys(['ZP', 'min_flux_pc_err'])! Setting to aper_diams=<Quantity [0.32] arcsec>
WARNING:galfind:cat_aper_diams not in kwargs.keys()=dict_keys([])! Setting to aper_diams=<Quantity [0.32] arcsec>
INFO:galfind:Made /raid/scratch/work/austind/GALFIND_WORK/Catalogues/v11/NIRCam/JOF/(0.32)as/JOF_MASTER_Sel-F277W+F356W+F444W_v11.fits catalogue!
****************************************
Galaxy(718, [53.10763,-27.86013]deg)
****************************************
PHOTOMETRY:
----------
Photometry_obs(NIRCam, 0.32 arcsec)
----------
SELECTION FLAGS:
----------
bluewards_Lya_SNR<2.0_EAZY_fsps_larson_zfree_0.32as: True
redwards_Lya_SNR>5.0,5.0_widebands_EAZY_fsps_larson_zfree_0.32as: True
ALL_redwards_Lya_SNR>2.0_widebands_EAZY_fsps_larson_zfree_0.32as: True
red_chi_sq<3.0_EAZY_fsps_larson_zfree_0.32as: True
chi_sq_diff>4.0,dz>0.5_EAZY_fsps_larson_zfree_0.32as: True
zPDF>60%,|dz|/z<0.1_EAZY_fsps_larson_zfree_0.32as: True
unmasked_F090W: True
unmasked_F115W: True
unmasked_F150W: True
unmasked_F162M: True
unmasked_F182M: True
unmasked_F200W: True
unmasked_F210M: True
unmasked_F250M: True
unmasked_F277W: True
unmasked_F300M: True
unmasked_F335M: True
unmasked_F356W: True
unmasked_F410M: True
unmasked_F444W: True
unmasked_NIRCam: True
bluest_band_SNR<2.0_0.32as: True
sex_Re_F277W>45.0mas: True
sex_Re_F356W>45.0mas: True
sex_Re_F444W>45.0mas: True
sex_Re_F277W+F356W+F444W>45.0mas: True
EPOCHS_NIRCam_EAZY_fsps_larson_zfree_0.32as: True
----------
****************************************

Example 1: Selecting unmasked galaxies and basic morphological selection

In this example, we will first have a look at selecting a galaxy based on whether it is unmasked in a particular band/bands/instrument. Following this, we will have a look at selecting a galaxy from its SExtractor measured half light radius. Basically, we are selecting galaxies independently of both the aperture diameter used to construct the photometry and any SED fitting properties derived from the SED fitting procedure.

This first example is probably the most important in this notebook as it highlights both the general syntax used as a basis for galfind sample selection as well as the properties which are stored in an individual Galaxy object as a result of the fitting procedure.

We start off by simply asserting that the galaxy is masked in the F444W band.

[3]:
from galfind import Unmasked_Band_Selector

unmasked_f444w_selector = Unmasked_Band_Selector("F444W")
print(unmasked_f444w_selector)
****************************************
Unmasked_Band_Selector:
****************************************
band_name: F444W
****************************************

[4]:
gal_1a = unmasked_f444w_selector(gal, return_copy = True)
print(gal_1a)
****************************************
Galaxy(718, [53.10763,-27.86013]deg)
****************************************
PHOTOMETRY:
----------
Photometry_obs(NIRCam, 0.32 arcsec)
----------
SELECTION FLAGS:
----------
bluewards_Lya_SNR<2.0_EAZY_fsps_larson_zfree_0.32as: True
redwards_Lya_SNR>5.0,5.0_widebands_EAZY_fsps_larson_zfree_0.32as: True
ALL_redwards_Lya_SNR>2.0_widebands_EAZY_fsps_larson_zfree_0.32as: True
red_chi_sq<3.0_EAZY_fsps_larson_zfree_0.32as: True
chi_sq_diff>4.0,dz>0.5_EAZY_fsps_larson_zfree_0.32as: True
zPDF>60%,|dz|/z<0.1_EAZY_fsps_larson_zfree_0.32as: True
unmasked_F090W: True
unmasked_F115W: True
unmasked_F150W: True
unmasked_F162M: True
unmasked_F182M: True
unmasked_F200W: True
unmasked_F210M: True
unmasked_F250M: True
unmasked_F277W: True
unmasked_F300M: True
unmasked_F335M: True
unmasked_F356W: True
unmasked_F410M: True
unmasked_F444W: True
unmasked_NIRCam: True
bluest_band_SNR<2.0_0.32as: True
sex_Re_F277W>45.0mas: True
sex_Re_F356W>45.0mas: True
sex_Re_F444W>45.0mas: True
sex_Re_F277W+F356W+F444W>45.0mas: True
EPOCHS_NIRCam_EAZY_fsps_larson_zfree_0.32as: True
----------
****************************************

From the two comparative Galaxy print statements, we see that the gal_1a object now has an Dict[str, bool] type selection_flags attribute stored, which can be extended with further selection criteria.

Now let’s have a go at selecting based on all selection bands being unmasked, those being the NIRCam LW widebands (F277W, F356W, F444W).

[5]:
from galfind import Unmasked_Bands_Selector

unmasked_selection_band_selector = Unmasked_Bands_Selector(forced_phot_band)
gal_1b = unmasked_selection_band_selector(gal_1a, return_copy = True)
print(gal_1b)

if isinstance(unmasked_selection_band_selector, tuple(Multiple_Selector.__subclasses__())):
    print(f"{repr(unmasked_selection_band_selector)} is a subclass of Multiple_Selector")
else:
    print(f"{repr(unmasked_selection_band_selector)} is not a subclass of Multiple_Selector")
****************************************
Galaxy(718, [53.10763,-27.86013]deg)
****************************************
PHOTOMETRY:
----------
Photometry_obs(NIRCam, 0.32 arcsec)
----------
SELECTION FLAGS:
----------
bluewards_Lya_SNR<2.0_EAZY_fsps_larson_zfree_0.32as: True
redwards_Lya_SNR>5.0,5.0_widebands_EAZY_fsps_larson_zfree_0.32as: True
ALL_redwards_Lya_SNR>2.0_widebands_EAZY_fsps_larson_zfree_0.32as: True
red_chi_sq<3.0_EAZY_fsps_larson_zfree_0.32as: True
chi_sq_diff>4.0,dz>0.5_EAZY_fsps_larson_zfree_0.32as: True
zPDF>60%,|dz|/z<0.1_EAZY_fsps_larson_zfree_0.32as: True
unmasked_F090W: True
unmasked_F115W: True
unmasked_F150W: True
unmasked_F162M: True
unmasked_F182M: True
unmasked_F200W: True
unmasked_F210M: True
unmasked_F250M: True
unmasked_F277W: True
unmasked_F300M: True
unmasked_F335M: True
unmasked_F356W: True
unmasked_F410M: True
unmasked_F444W: True
unmasked_NIRCam: True
bluest_band_SNR<2.0_0.32as: True
sex_Re_F277W>45.0mas: True
sex_Re_F356W>45.0mas: True
sex_Re_F444W>45.0mas: True
sex_Re_F277W+F356W+F444W>45.0mas: True
EPOCHS_NIRCam_EAZY_fsps_larson_zfree_0.32as: True
unmasked_F277W+F356W+F444W: True
----------
****************************************

Unmasked_Bands_Selector() is a subclass of Multiple_Selector

The selection object above is a child class of the base Multiple_Selector. This class, which itself inherits from the Selector class contains an array of Selector objects with assertion/failure/selection criteria defined by the relevant boolean logic to select only galaxies that pass all of the individual selection criteria. When calling the Multiple_Selector object, selection is performed and saved from each individual Selector object stored in the list before finally running the combined selection. It is for this reason why the gal_1b object above contains 4 entries in its selection_flags dictionary attribute (unmasked in F277W, F356W, F444W, and F277W+F356W+F444W).

We see that this is identical to the implementation where instead of inserting an array of band names to mask, we insert a single string with band names seperated by +s.

[6]:
unmasked_selection_band_selector_2 = Unmasked_Bands_Selector("+".join(forced_phot_band))
gal_1c = unmasked_selection_band_selector_2(gal_1a, return_copy = True)
if gal_1b == gal_1c:
    print("Identical implementation!")
else:
    print("Different implementation!")
Different implementation!

Finally, we will ensure that all NIRCam bands contained within the JOF survey are unmasked.

We note that the implementation here is slightly different to the one performed on a catalogue level. Here we have to first insert a Multiple_Filter object to reduce the relevant bands from all available bandpass filters on the instrument to just those available in our survey. We cannot simply use the filterset on the galaxy object itself as this may already have been trimmed to include just bands that include data at the specific sky position of the galaxy itself, rather than all those available in the field. If we were to instead ask for every band contained in the galaxy object to be included, then when running on a catalogue level this selection function would return True even when there may not be data available in the band we wish to be masked.

[7]:
from galfind import Unmasked_Instrument_Selector

unmasked_nircam_selector = Unmasked_Instrument_Selector("NIRCam")
unmasked_nircam_selector.crop_to_filterset(cat.filterset)
gal_1d = unmasked_nircam_selector(gal_1b, return_copy = True)
print(gal_1d)
****************************************
Galaxy(718, [53.10763,-27.86013]deg)
****************************************
PHOTOMETRY:
----------
Photometry_obs(NIRCam, 0.32 arcsec)
----------
SELECTION FLAGS:
----------
bluewards_Lya_SNR<2.0_EAZY_fsps_larson_zfree_0.32as: True
redwards_Lya_SNR>5.0,5.0_widebands_EAZY_fsps_larson_zfree_0.32as: True
ALL_redwards_Lya_SNR>2.0_widebands_EAZY_fsps_larson_zfree_0.32as: True
red_chi_sq<3.0_EAZY_fsps_larson_zfree_0.32as: True
chi_sq_diff>4.0,dz>0.5_EAZY_fsps_larson_zfree_0.32as: True
zPDF>60%,|dz|/z<0.1_EAZY_fsps_larson_zfree_0.32as: True
unmasked_F090W: True
unmasked_F115W: True
unmasked_F150W: True
unmasked_F162M: True
unmasked_F182M: True
unmasked_F200W: True
unmasked_F210M: True
unmasked_F250M: True
unmasked_F277W: True
unmasked_F300M: True
unmasked_F335M: True
unmasked_F356W: True
unmasked_F410M: True
unmasked_F444W: True
unmasked_NIRCam: True
bluest_band_SNR<2.0_0.32as: True
sex_Re_F277W>45.0mas: True
sex_Re_F356W>45.0mas: True
sex_Re_F444W>45.0mas: True
sex_Re_F277W+F356W+F444W>45.0mas: True
EPOCHS_NIRCam_EAZY_fsps_larson_zfree_0.32as: True
unmasked_F277W+F356W+F444W: True
----------
****************************************

The above object now contains selection_flags entries for each band in the JOF field as well as one for the forced photometry band (F277W+F356W+F444W) and one for the NIRCam instrument, all reading True. It seems that this galaxy has data in all NIRCam bands that the survey is constructed from and is not masked by our manual mask in any band.

As well as selecting a galaxy based on (an) unmasked band/bands/instrument, there is one more aperture diameter independent selection method that we implement in galfind: SExtractor half-light radius selection. This is mainly used to remove hot pixels that survive the data reduction process from our galaxy samples, although in theory it can be used to exclude larger and point like galaxies from a sample should you wish. In the future, we aim to include more advanced morphological fitting and selection in SExtractor from commonly used Sersic profile fitting codes such as GALFIT, Imfit, Morphometryka (currently private), etc used by e.g. Ormerod+23, Westcott+24.

To do this, we will first have to load the galaxy half-light radius from the fits catalogue, which we will do on the catalogue level.

[8]:
cat.load_sextractor_Re()
gal = cat[717]
INFO:galfind:Loaded FLUX_RADIUS from /raid/scratch/work/austind/GALFIND_WORK/Catalogues/v11/NIRCam/JOF/(0.32)as/JOF_MASTER_Sel-F277W+F356W+F444W_v11.fits saved as sex_Re for cat_band_properties[0].keys()=dict_keys(['F090W', 'F115W', 'F150W', 'F162M', 'F182M', 'F200W', 'F210M', 'F250M', 'F277W', 'F300M', 'F335M', 'F356W', 'F410M', 'F444W'])

Now that the new galaxy properties have been loaded, let’s have a go at excluding F444W hot pixels with R_e < 1.5 pix (i.e. selecting objects with > 1.5 pix SExtractor half light radii). Since our pixel scale is 0.03as/pix, we select > 45mas objects.

[9]:
from galfind import Sextractor_Band_Radius_Selector

Re_gtr_45mas_f444w_selector = Sextractor_Band_Radius_Selector("F444W", "gtr", 45. * u.marcsec)
gal_1e = Re_gtr_45mas_f444w_selector(gal, return_copy = True)
print(gal_1e)
****************************************
Galaxy(718, [53.10763,-27.86013]deg)
****************************************
PHOTOMETRY:
----------
Photometry_obs(NIRCam, 0.32 arcsec)
----------
SELECTION FLAGS:
----------
bluewards_Lya_SNR<2.0_EAZY_fsps_larson_zfree_0.32as: True
redwards_Lya_SNR>5.0,5.0_widebands_EAZY_fsps_larson_zfree_0.32as: True
ALL_redwards_Lya_SNR>2.0_widebands_EAZY_fsps_larson_zfree_0.32as: True
red_chi_sq<3.0_EAZY_fsps_larson_zfree_0.32as: True
chi_sq_diff>4.0,dz>0.5_EAZY_fsps_larson_zfree_0.32as: True
zPDF>60%,|dz|/z<0.1_EAZY_fsps_larson_zfree_0.32as: True
unmasked_F090W: True
unmasked_F115W: True
unmasked_F150W: True
unmasked_F162M: True
unmasked_F182M: True
unmasked_F200W: True
unmasked_F210M: True
unmasked_F250M: True
unmasked_F277W: True
unmasked_F300M: True
unmasked_F335M: True
unmasked_F356W: True
unmasked_F410M: True
unmasked_F444W: True
unmasked_NIRCam: True
bluest_band_SNR<2.0_0.32as: True
sex_Re_F277W>45.0mas: True
sex_Re_F356W>45.0mas: True
sex_Re_F444W>45.0mas: True
sex_Re_F277W+F356W+F444W>45.0mas: True
EPOCHS_NIRCam_EAZY_fsps_larson_zfree_0.32as: True
----------
****************************************

We can also perform this selection to select objects with smaller half-light radii.

[10]:
Re_less_50mas_f444w_selector = Sextractor_Band_Radius_Selector("F444W", "less", 50. * u.marcsec)
gal_1f = Re_less_50mas_f444w_selector(gal_1e, return_copy = True)
print(gal_1f)
****************************************
Galaxy(718, [53.10763,-27.86013]deg)
****************************************
PHOTOMETRY:
----------
Photometry_obs(NIRCam, 0.32 arcsec)
----------
SELECTION FLAGS:
----------
bluewards_Lya_SNR<2.0_EAZY_fsps_larson_zfree_0.32as: True
redwards_Lya_SNR>5.0,5.0_widebands_EAZY_fsps_larson_zfree_0.32as: True
ALL_redwards_Lya_SNR>2.0_widebands_EAZY_fsps_larson_zfree_0.32as: True
red_chi_sq<3.0_EAZY_fsps_larson_zfree_0.32as: True
chi_sq_diff>4.0,dz>0.5_EAZY_fsps_larson_zfree_0.32as: True
zPDF>60%,|dz|/z<0.1_EAZY_fsps_larson_zfree_0.32as: True
unmasked_F090W: True
unmasked_F115W: True
unmasked_F150W: True
unmasked_F162M: True
unmasked_F182M: True
unmasked_F200W: True
unmasked_F210M: True
unmasked_F250M: True
unmasked_F277W: True
unmasked_F300M: True
unmasked_F335M: True
unmasked_F356W: True
unmasked_F410M: True
unmasked_F444W: True
unmasked_NIRCam: True
bluest_band_SNR<2.0_0.32as: True
sex_Re_F277W>45.0mas: True
sex_Re_F356W>45.0mas: True
sex_Re_F444W>45.0mas: True
sex_Re_F277W+F356W+F444W>45.0mas: True
EPOCHS_NIRCam_EAZY_fsps_larson_zfree_0.32as: True
sex_Re_F444W<50.0mas: False
----------
****************************************

As before, we can also run the Multiple_Selector version of this same selection method by instead inserting a list (or single string separated by +s) of filter names.

[11]:
from galfind import Sextractor_Bands_Radius_Selector

Re_gtr_45mas_forced_phot_band_selector = Sextractor_Bands_Radius_Selector(forced_phot_band, "gtr", 45. * u.marcsec)
gal_1g = Re_gtr_45mas_forced_phot_band_selector(gal_1f, return_copy = True)
print(gal_1g)
****************************************
Galaxy(718, [53.10763,-27.86013]deg)
****************************************
PHOTOMETRY:
----------
Photometry_obs(NIRCam, 0.32 arcsec)
----------
SELECTION FLAGS:
----------
bluewards_Lya_SNR<2.0_EAZY_fsps_larson_zfree_0.32as: True
redwards_Lya_SNR>5.0,5.0_widebands_EAZY_fsps_larson_zfree_0.32as: True
ALL_redwards_Lya_SNR>2.0_widebands_EAZY_fsps_larson_zfree_0.32as: True
red_chi_sq<3.0_EAZY_fsps_larson_zfree_0.32as: True
chi_sq_diff>4.0,dz>0.5_EAZY_fsps_larson_zfree_0.32as: True
zPDF>60%,|dz|/z<0.1_EAZY_fsps_larson_zfree_0.32as: True
unmasked_F090W: True
unmasked_F115W: True
unmasked_F150W: True
unmasked_F162M: True
unmasked_F182M: True
unmasked_F200W: True
unmasked_F210M: True
unmasked_F250M: True
unmasked_F277W: True
unmasked_F300M: True
unmasked_F335M: True
unmasked_F356W: True
unmasked_F410M: True
unmasked_F444W: True
unmasked_NIRCam: True
bluest_band_SNR<2.0_0.32as: True
sex_Re_F277W>45.0mas: True
sex_Re_F356W>45.0mas: True
sex_Re_F444W>45.0mas: True
sex_Re_F277W+F356W+F444W>45.0mas: True
EPOCHS_NIRCam_EAZY_fsps_larson_zfree_0.32as: True
sex_Re_F444W<50.0mas: False
----------
****************************************

And again we can run the selection on all bands from a single instrument, which in this case is NIRCam.

[12]:
from galfind import Sextractor_Instrument_Radius_Selector

Re_gtr_45mas_nircam_selector = Sextractor_Instrument_Radius_Selector("NIRCam", "gtr", 45. * u.marcsec)
Re_gtr_45mas_nircam_selector.crop_to_filterset(cat.filterset)
gal_1h = Re_gtr_45mas_nircam_selector(gal_1g, return_copy = True)
print(gal_1h)
****************************************
Galaxy(718, [53.10763,-27.86013]deg)
****************************************
PHOTOMETRY:
----------
Photometry_obs(NIRCam, 0.32 arcsec)
----------
SELECTION FLAGS:
----------
bluewards_Lya_SNR<2.0_EAZY_fsps_larson_zfree_0.32as: True
redwards_Lya_SNR>5.0,5.0_widebands_EAZY_fsps_larson_zfree_0.32as: True
ALL_redwards_Lya_SNR>2.0_widebands_EAZY_fsps_larson_zfree_0.32as: True
red_chi_sq<3.0_EAZY_fsps_larson_zfree_0.32as: True
chi_sq_diff>4.0,dz>0.5_EAZY_fsps_larson_zfree_0.32as: True
zPDF>60%,|dz|/z<0.1_EAZY_fsps_larson_zfree_0.32as: True
unmasked_F090W: True
unmasked_F115W: True
unmasked_F150W: True
unmasked_F162M: True
unmasked_F182M: True
unmasked_F200W: True
unmasked_F210M: True
unmasked_F250M: True
unmasked_F277W: True
unmasked_F300M: True
unmasked_F335M: True
unmasked_F356W: True
unmasked_F410M: True
unmasked_F444W: True
unmasked_NIRCam: True
bluest_band_SNR<2.0_0.32as: True
sex_Re_F277W>45.0mas: True
sex_Re_F356W>45.0mas: True
sex_Re_F444W>45.0mas: True
sex_Re_F277W+F356W+F444W>45.0mas: True
EPOCHS_NIRCam_EAZY_fsps_larson_zfree_0.32as: True
sex_Re_F444W<50.0mas: False
sex_Re_F090W>45.0mas: True
sex_Re_F115W>45.0mas: False
sex_Re_F150W>45.0mas: False
sex_Re_F162M>45.0mas: False
sex_Re_F182M>45.0mas: False
sex_Re_F200W>45.0mas: True
sex_Re_F210M>45.0mas: False
sex_Re_F250M>45.0mas: True
sex_Re_F300M>45.0mas: True
sex_Re_F335M>45.0mas: True
sex_Re_F410M>45.0mas: True
sex_Re_NIRCam>45.0mas: False
----------
****************************************

We caution the user that instrument half-light radii selection may not be a smart idea in photometric bands with low SNR detections. In this case, our high redshift galaxy candidate fails this selection as it is not detected in the bluer dropout NIRCam bands where SExtractor struggles to fit the galaxy with an appropriate Kron radius.

In this example, we kept on making copies of the galaxy in question, which is the default implementation when running on a galaxy rather than catalogue level. From now on, we will explicitly insert return_copy = False, so that the galaxy object itself is updated, rather than its deepcopy.

[13]:
# TODO: Update documentation here!
#from galfind import Min_Band_Selector, Min_Unmasked_Band_Selector

Example 2: SNRs and colours

Now let’s try a simple extension of the previous example by implementating SNR and colour selections. This requires knowledge of which aperture photometry this is being run on, so we must explicitly insert the aperture diameter we wish to perform the selection on here when calling the Selector object. This adds a small amount of additional complexity to this selection method.

[14]:
from galfind import Band_SNR_Selector, Colour_Selector, Kokorev24_LRD_Selector

We will start with a single band SNR selection, based on whether a galaxy is detected/undetected at a specific significance. This significance is set by the ratio of aperture measured flux compared to the local background level determined by the depth measurements performed by Data. We will start by performing selection for 5 sigma F444W detections, which has the following syntax.

[15]:
f444w_gtr_5sigma_selector = Band_SNR_Selector(aper_diams[0], "F444W", "detect", 5.)
f444w_gtr_5sigma_selector(gal, return_copy = False)

As well as inserting the band name directly, we can instead require that the n^th photometric filter that the galaxy has data for be (non-)detected to a specific significance level by inserting an integer, rather than band name. For instance, should we require that the second reddest band be selected at greater than, say 8 sigma, and the second bluest be non detected to 2 sigma, we can write…

[16]:
second_reddest_gtr_8sigma_selector = Band_SNR_Selector(aper_diams[0], -2, "detect", 8.)
second_reddest_gtr_8sigma_selector(gal, return_copy = False)

bluest_less_2sigma_selector = Band_SNR_Selector(aper_diams[0], 0, "non_detect", 2.)
bluest_less_2sigma_selector(gal, return_copy = False)

Fantastic! But what about colour selection? This is also very straightforwards in galfind. A couple of random examples are given below. Note that the filter names defining the colour to be calculated can be given either as a list of length 2 or a single string separated by a - sign. In both cases these should be ordered blue -> red!

[17]:
f200w_f277w_bluer_0_3sigma_selector = Colour_Selector(aper_diams[0], ["F200W", "F277W"], "bluer", 0.3)
f200w_f277w_bluer_0_3sigma_selector(gal, return_copy = False)

f356w_f444w_redder_0_5sigma_selector = Colour_Selector(aper_diams[0], "F356W-F444W", "redder", 0.5)
f356w_f444w_redder_0_5sigma_selector(gal, return_copy = False)

As well as basic colour selection, it is common to select galaxy samples based on multiple colour criteria. Here we will use the example of the LRD selection from Kokorev et al. 2024, which combines multiple photometric colour criteria.

[18]:
Kokorev24_selector = Kokorev24_LRD_Selector(aper_diams[0])
Kokorev24_selector(gal, return_copy = False)

print(gal)
****************************************
Galaxy(718, [53.10763,-27.86013]deg)
****************************************
PHOTOMETRY:
----------
Photometry_obs(NIRCam, 0.32 arcsec)
----------
SELECTION FLAGS:
----------
bluewards_Lya_SNR<2.0_EAZY_fsps_larson_zfree_0.32as: True
redwards_Lya_SNR>5.0,5.0_widebands_EAZY_fsps_larson_zfree_0.32as: True
ALL_redwards_Lya_SNR>2.0_widebands_EAZY_fsps_larson_zfree_0.32as: True
red_chi_sq<3.0_EAZY_fsps_larson_zfree_0.32as: True
chi_sq_diff>4.0,dz>0.5_EAZY_fsps_larson_zfree_0.32as: True
zPDF>60%,|dz|/z<0.1_EAZY_fsps_larson_zfree_0.32as: True
unmasked_F090W: True
unmasked_F115W: True
unmasked_F150W: True
unmasked_F162M: True
unmasked_F182M: True
unmasked_F200W: True
unmasked_F210M: True
unmasked_F250M: True
unmasked_F277W: True
unmasked_F300M: True
unmasked_F335M: True
unmasked_F356W: True
unmasked_F410M: True
unmasked_F444W: True
unmasked_NIRCam: True
bluest_band_SNR<2.0_0.32as: True
sex_Re_F277W>45.0mas: True
sex_Re_F356W>45.0mas: True
sex_Re_F444W>45.0mas: True
sex_Re_F277W+F356W+F444W>45.0mas: True
EPOCHS_NIRCam_EAZY_fsps_larson_zfree_0.32as: True
F444W_SNR>5.0_0.32as: True
2nd_reddest_band_SNR>8.0_0.32as: False
F200W-F277W<0.30_0.32as: False
F356W-F444W>0.50_0.32as: False
F115W-F150W<0.80_0.32as: False
F200W-F277W>0.70_0.32as: True
F200W-F356W>1.00_0.32as: False
Kokorev+24_LRD_red1_0.32as: False
F150W-F200W<0.80_0.32as: False
F277W-F356W>0.60_0.32as: False
F277W-F444W>0.70_0.32as: False
Kokorev+24_LRD_red2_0.32as: False
Kokorev+24_LRD_0.32as: False
----------
****************************************

/nvme/scratch/software/anaconda3/envs/more_and_more_galfind/lib/python3.9/site-packages/astropy/utils/masked/core.py:855: RuntimeWarning: invalid value encountered in log10
  result = getattr(ufunc, method)(*unmasked, **kwargs)

It is worth noting that a general colour-colour selection criteria object has not been implemented in galfind, although this can easily be done using the custom selector object explained in example 6.

Example 3: Robust redshift selection criteria

In this next example, we will add one more layer of complexity in that we will now select galaxies based on the output of previously performed SED fitting, assuming that PDFs and/or SEDs have already been loaded in the Galaxy object. There are a few different in-built selector classes which specifically focus on photometric redshift robustness, which we will outline here.

First of all, we should load in the results of the EAZY_fsps_larson SED fitting with lowz_zmax={4., 6., None}. Instead of loading in the specific redshift PDF, best-fitting SED, and properties/errors for the individual galaxy, here we will load these in for the entire JOF catalogue before cropping back down to the same ID=718 high-z candidate again. Please note that the code-block below will perform the SED fitting if not already performed, although in our case it will simply load the results into the catalogue.

[19]:
from galfind import EAZY

SED_fit_params_arr = [
    {"templates": "fsps_larson", "lowz_zmax": 4.0},
    {"templates": "fsps_larson", "lowz_zmax": 6.0},
    {"templates": "fsps_larson", "lowz_zmax": None}
]
for SED_fit_params in SED_fit_params_arr:
    EAZY_fitter = EAZY(SED_fit_params)
    EAZY_fitter(cat, aper_diams[0], load_PDFs = True, load_SEDs = True, update = True)

gal = cat[717]
print(gal)

gal_copy = deepcopy(gal)
INFO:galfind:Making .in file for EAZY_fsps_larson_zmax=4.0 SED fitting for JOF v11 NIRCam
INFO:galfind:Made .in file for EAZY_fsps_larson_zmax=4.0 SED fitting for JOF v11 NIRCam.
Running SED fitting took 0.1s
INFO:galfind:Loading EAZY_fsps_larson property PDFs into JOF v11 NIRCam
Loading properties and associated errors took 1.7s
Constructing redshift PDFs: 100%|██████████| 16335/16335 [00:00<00:00, 51820.38it/s]
INFO:galfind:Finished loading EAZY_fsps_larson property PDFs into JOF v11 NIRCam
INFO:galfind:Loading EAZY_fsps_larson SEDs into JOF v11 NIRCam
Constructing SEDs: 100%|██████████| 16335/16335 [00:01<00:00, 13137.21it/s]
INFO:galfind:Finished loading EAZY_fsps_larson SEDs into JOF v11 NIRCam
INFO:galfind:Updating SED results in galfind catalogue object
Updating galaxy SED results: 100%|██████████| 16335/16335 [00:00<00:00, 97403.14it/s]
INFO:galfind:Making .in file for EAZY_fsps_larson_zmax=6.0 SED fitting for JOF v11 NIRCam
INFO:galfind:Made .in file for EAZY_fsps_larson_zmax=6.0 SED fitting for JOF v11 NIRCam.
Running SED fitting took 0.1s
INFO:galfind:Loading EAZY_fsps_larson property PDFs into JOF v11 NIRCam
Loading properties and associated errors took 1.0s
Constructing redshift PDFs: 100%|██████████| 16335/16335 [00:00<00:00, 49979.58it/s]
INFO:galfind:Finished loading EAZY_fsps_larson property PDFs into JOF v11 NIRCam
INFO:galfind:Loading EAZY_fsps_larson SEDs into JOF v11 NIRCam
Constructing SEDs: 100%|██████████| 16335/16335 [00:01<00:00, 11128.17it/s]
INFO:galfind:Finished loading EAZY_fsps_larson SEDs into JOF v11 NIRCam
INFO:galfind:Updating SED results in galfind catalogue object
Updating galaxy SED results: 100%|██████████| 16335/16335 [00:00<00:00, 113332.75it/s]
INFO:galfind:Making .in file for EAZY_fsps_larson_zfree SED fitting for JOF v11 NIRCam
INFO:galfind:Made .in file for EAZY_fsps_larson_zfree SED fitting for JOF v11 NIRCam.
Running SED fitting took 0.1s
INFO:galfind:Loading EAZY_fsps_larson property PDFs into JOF v11 NIRCam
Loading properties and associated errors took 2.0s
Constructing redshift PDFs: 100%|██████████| 16335/16335 [00:00<00:00, 45034.97it/s]
INFO:galfind:Finished loading EAZY_fsps_larson property PDFs into JOF v11 NIRCam
INFO:galfind:Loading EAZY_fsps_larson SEDs into JOF v11 NIRCam
Constructing SEDs: 100%|██████████| 16335/16335 [00:01<00:00, 13250.56it/s]
INFO:galfind:Finished loading EAZY_fsps_larson SEDs into JOF v11 NIRCam
INFO:galfind:Updating SED results in galfind catalogue object
Updating galaxy SED results: 100%|██████████| 16335/16335 [00:00<00:00, 170640.30it/s]
****************************************
Galaxy(718, [53.10763,-27.86013]deg)
****************************************
PHOTOMETRY:
----------
Photometry_obs(NIRCam, 0.32 arcsec, EAZY_fsps_larson_zmax=4.0,EAZY_fsps_larson_zmax=6.0,EAZY_fsps_larson_zfree)
----------
SELECTION FLAGS:
----------
bluewards_Lya_SNR<2.0_EAZY_fsps_larson_zfree_0.32as: True
redwards_Lya_SNR>5.0,5.0_widebands_EAZY_fsps_larson_zfree_0.32as: True
ALL_redwards_Lya_SNR>2.0_widebands_EAZY_fsps_larson_zfree_0.32as: True
red_chi_sq<3.0_EAZY_fsps_larson_zfree_0.32as: True
chi_sq_diff>4.0,dz>0.5_EAZY_fsps_larson_zfree_0.32as: True
zPDF>60%,|dz|/z<0.1_EAZY_fsps_larson_zfree_0.32as: True
unmasked_F090W: True
unmasked_F115W: True
unmasked_F150W: True
unmasked_F162M: True
unmasked_F182M: True
unmasked_F200W: True
unmasked_F210M: True
unmasked_F250M: True
unmasked_F277W: True
unmasked_F300M: True
unmasked_F335M: True
unmasked_F356W: True
unmasked_F410M: True
unmasked_F444W: True
unmasked_NIRCam: True
bluest_band_SNR<2.0_0.32as: True
sex_Re_F277W>45.0mas: True
sex_Re_F356W>45.0mas: True
sex_Re_F444W>45.0mas: True
sex_Re_F277W+F356W+F444W>45.0mas: True
EPOCHS_NIRCam_EAZY_fsps_larson_zfree_0.32as: True
F444W_SNR>5.0_0.32as: True
2nd_reddest_band_SNR>8.0_0.32as: False
F200W-F277W<0.30_0.32as: False
F356W-F444W>0.50_0.32as: False
F115W-F150W<0.80_0.32as: False
F200W-F277W>0.70_0.32as: True
F200W-F356W>1.00_0.32as: False
Kokorev+24_LRD_red1_0.32as: False
F150W-F200W<0.80_0.32as: False
F277W-F356W>0.60_0.32as: False
F277W-F444W>0.70_0.32as: False
Kokorev+24_LRD_red2_0.32as: False
Kokorev+24_LRD_0.32as: False
----------
****************************************


[20]:
from galfind import (
    Bluewards_Lya_Non_Detect_Selector,
    Redwards_Lya_Detect_Selector,
    Lya_Band_Selector,
    Chi_Sq_Lim_Selector,
    Chi_Sq_Diff_Selector,
    Robust_zPDF_Selector,
)

Let’s start by going over SNR-based selection either side of the Lyman break involving the Bluewards_Lya_Non_Detect_Selector, Redwards_Lya_Detect_Selector, and Lya_Band_Selector.

[21]:
two_sig_non_detect_bluewards_Lya_selector = Bluewards_Lya_Non_Detect_Selector(aper_diams[0], EAZY_fitter, SNR_lim = 2.0)
two_sig_non_detect_bluewards_Lya_selector(gal, return_copy = False)

two_sigma_detect_all_redwards_Lya_selector = Redwards_Lya_Detect_Selector(aper_diams[0], EAZY_fitter, SNR_lims = 2.0, widebands_only = False)
two_sigma_detect_all_redwards_Lya_selector(gal, return_copy = False)

five_sigma_detect_two_redwards_Lya_selector = Redwards_Lya_Detect_Selector(aper_diams[0], EAZY_fitter, SNR_lims = [5.0, 5.0], widebands_only = True)
five_sigma_detect_two_redwards_Lya_selector(gal, return_copy = False)

print(gal)
****************************************
Galaxy(718, [53.10763,-27.86013]deg)
****************************************
PHOTOMETRY:
----------
Photometry_obs(NIRCam, 0.32 arcsec, EAZY_fsps_larson_zmax=4.0,EAZY_fsps_larson_zmax=6.0,EAZY_fsps_larson_zfree)
----------
SELECTION FLAGS:
----------
bluewards_Lya_SNR<2.0_EAZY_fsps_larson_zfree_0.32as: True
redwards_Lya_SNR>5.0,5.0_widebands_EAZY_fsps_larson_zfree_0.32as: True
ALL_redwards_Lya_SNR>2.0_widebands_EAZY_fsps_larson_zfree_0.32as: True
red_chi_sq<3.0_EAZY_fsps_larson_zfree_0.32as: True
chi_sq_diff>4.0,dz>0.5_EAZY_fsps_larson_zfree_0.32as: True
zPDF>60%,|dz|/z<0.1_EAZY_fsps_larson_zfree_0.32as: True
unmasked_F090W: True
unmasked_F115W: True
unmasked_F150W: True
unmasked_F162M: True
unmasked_F182M: True
unmasked_F200W: True
unmasked_F210M: True
unmasked_F250M: True
unmasked_F277W: True
unmasked_F300M: True
unmasked_F335M: True
unmasked_F356W: True
unmasked_F410M: True
unmasked_F444W: True
unmasked_NIRCam: True
bluest_band_SNR<2.0_0.32as: True
sex_Re_F277W>45.0mas: True
sex_Re_F356W>45.0mas: True
sex_Re_F444W>45.0mas: True
sex_Re_F277W+F356W+F444W>45.0mas: True
EPOCHS_NIRCam_EAZY_fsps_larson_zfree_0.32as: True
F444W_SNR>5.0_0.32as: True
2nd_reddest_band_SNR>8.0_0.32as: False
F200W-F277W<0.30_0.32as: False
F356W-F444W>0.50_0.32as: False
F115W-F150W<0.80_0.32as: False
F200W-F277W>0.70_0.32as: True
F200W-F356W>1.00_0.32as: False
Kokorev+24_LRD_red1_0.32as: False
F150W-F200W<0.80_0.32as: False
F277W-F356W>0.60_0.32as: False
F277W-F444W>0.70_0.32as: False
Kokorev+24_LRD_red2_0.32as: False
Kokorev+24_LRD_0.32as: False
ALL_redwards_Lya_SNR>2.0_EAZY_fsps_larson_zfree_0.32as: True
----------
****************************************

The above are some of the selection criteria used by the EPOCHS paper series. The first is the requirement of 2 sigma non-detections in all bands bluer than the Lyman alpha break at 1216 Angstrom. The second is the requirement that ALL bands redwards of Lyman-alpha have 2 sigma detections. Finally, the first two widebands redwards of the Lyman-alpha break must be detected to at least 5 sigma significance. These SNRs are, of course, determined by the depth measurements performed by Data, which are explained in the Depths notebook.

As explained in Adams et al. 2025, the high-z JOF galaxy used in this example notebook is selected by these SNR criteria, which impose a strong Lya break strength.

Let’s now have a little play around with the options available in the Lya_Band_Selector class and the corresponding selection_flags dictionary keys that are created.

[22]:
lya_band_2sig_detect_selector = Lya_Band_Selector(aper_diams[0], EAZY_fitter, SNR_lim = 2.0, detect_or_non_detect = "detect", widebands_only = False)
lya_band_2sig_detect_selector(gal, return_copy = False)

lya_wideband_3sig_non_detect_selector = Lya_Band_Selector(aper_diams[0], EAZY_fitter, SNR_lim = 3.0, detect_or_non_detect = "non_detect", widebands_only = True)
lya_wideband_3sig_non_detect_selector(gal, return_copy = False)

As well as various SNR criteria in bands determined by the position of the Lyman-alpha break at the SED fitting redshift, the EPOCHS criteria also imposes that the best-fit redshifts are robust, with low chi-square dominating the best-fit chi square of the low-redshift interloper runs, and a narrow redshift PDF peak surrounding the best-fit solution.

[23]:
red_chi_sq_less_3_selector = Chi_Sq_Lim_Selector(aper_diams[0], EAZY_fitter, chi_sq_lim = 3.0, reduced = True)
red_chi_sq_less_3_selector(gal, return_copy = False)

red_chi_sq_diff_less_4_dz_0_5_selector = Chi_Sq_Diff_Selector(aper_diams[0], EAZY_fitter, chi_sq_diff = 4.0, dz = 0.5)
red_chi_sq_diff_less_4_dz_0_5_selector(gal, return_copy = False)

robust_zpdf_gtr0_6_dz_0_1_z_selector = Robust_zPDF_Selector(aper_diams[0], EAZY_fitter, integral_lim = 0.6, dz_over_z = 0.1)
robust_zpdf_gtr0_6_dz_0_1_z_selector(gal, return_copy = False)

print(gal.selection_flags.keys())
dict_keys(['bluewards_Lya_SNR<2.0_EAZY_fsps_larson_zfree_0.32as', 'redwards_Lya_SNR>5.0,5.0_widebands_EAZY_fsps_larson_zfree_0.32as', 'ALL_redwards_Lya_SNR>2.0_widebands_EAZY_fsps_larson_zfree_0.32as', 'red_chi_sq<3.0_EAZY_fsps_larson_zfree_0.32as', 'chi_sq_diff>4.0,dz>0.5_EAZY_fsps_larson_zfree_0.32as', 'zPDF>60%,|dz|/z<0.1_EAZY_fsps_larson_zfree_0.32as', 'unmasked_F090W', 'unmasked_F115W', 'unmasked_F150W', 'unmasked_F162M', 'unmasked_F182M', 'unmasked_F200W', 'unmasked_F210M', 'unmasked_F250M', 'unmasked_F277W', 'unmasked_F300M', 'unmasked_F335M', 'unmasked_F356W', 'unmasked_F410M', 'unmasked_F444W', 'unmasked_NIRCam', 'bluest_band_SNR<2.0_0.32as', 'sex_Re_F277W>45.0mas', 'sex_Re_F356W>45.0mas', 'sex_Re_F444W>45.0mas', 'sex_Re_F277W+F356W+F444W>45.0mas', 'EPOCHS_NIRCam_EAZY_fsps_larson_zfree_0.32as', 'F444W_SNR>5.0_0.32as', '2nd_reddest_band_SNR>8.0_0.32as', 'F200W-F277W<0.30_0.32as', 'F356W-F444W>0.50_0.32as', 'F115W-F150W<0.80_0.32as', 'F200W-F277W>0.70_0.32as', 'F200W-F356W>1.00_0.32as', 'Kokorev+24_LRD_red1_0.32as', 'F150W-F200W<0.80_0.32as', 'F277W-F356W>0.60_0.32as', 'F277W-F444W>0.70_0.32as', 'Kokorev+24_LRD_red2_0.32as', 'Kokorev+24_LRD_0.32as', 'ALL_redwards_Lya_SNR>2.0_EAZY_fsps_larson_zfree_0.32as', 'Lya_band_SNR>2.0_EAZY_fsps_larson_zfree_0.32as', 'Lya_band_SNR<3.0_widebands_EAZY_fsps_larson_zfree_0.32as'])

Example 4: EPOCHS selection in galfind

After learning all the individual selection criteria that can be implemented in the above examples, we will now put these all together and re-produce the EPOCHS criteria from the EPOCHS paper series. Here we must explicitly insert the catalogue filterset so that the unmasked instrument criteria can be appropriately applied to the single Galaxy object that selection is being run for.

[24]:
from galfind import EPOCHS_Selector

epochs_selector = EPOCHS_Selector(aper_diams[0], EAZY_fitter, allow_lowz = False, unmasked_instruments = "NIRCam", cat_filterset = cat.filterset)
epochs_selected_gal = epochs_selector(gal_copy, return_copy = True)

print(epochs_selected_gal)
****************************************
Galaxy(718, [53.10763,-27.86013]deg)
****************************************
PHOTOMETRY:
----------
Photometry_obs(NIRCam, 0.32 arcsec, EAZY_fsps_larson_zmax=4.0,EAZY_fsps_larson_zmax=6.0,EAZY_fsps_larson_zfree)
----------
SELECTION FLAGS:
----------
bluewards_Lya_SNR<2.0_EAZY_fsps_larson_zfree_0.32as: True
redwards_Lya_SNR>5.0,5.0_widebands_EAZY_fsps_larson_zfree_0.32as: True
ALL_redwards_Lya_SNR>2.0_widebands_EAZY_fsps_larson_zfree_0.32as: True
red_chi_sq<3.0_EAZY_fsps_larson_zfree_0.32as: True
chi_sq_diff>4.0,dz>0.5_EAZY_fsps_larson_zfree_0.32as: True
zPDF>60%,|dz|/z<0.1_EAZY_fsps_larson_zfree_0.32as: True
unmasked_F090W: True
unmasked_F115W: True
unmasked_F150W: True
unmasked_F162M: True
unmasked_F182M: True
unmasked_F200W: True
unmasked_F210M: True
unmasked_F250M: True
unmasked_F277W: True
unmasked_F300M: True
unmasked_F335M: True
unmasked_F356W: True
unmasked_F410M: True
unmasked_F444W: True
unmasked_NIRCam: True
bluest_band_SNR<2.0_0.32as: True
sex_Re_F277W>45.0mas: True
sex_Re_F356W>45.0mas: True
sex_Re_F444W>45.0mas: True
sex_Re_F277W+F356W+F444W>45.0mas: True
EPOCHS_NIRCam_EAZY_fsps_larson_zfree_0.32as: True
F444W_SNR>5.0_0.32as: True
2nd_reddest_band_SNR>8.0_0.32as: False
F200W-F277W<0.30_0.32as: False
F356W-F444W>0.50_0.32as: False
F115W-F150W<0.80_0.32as: False
F200W-F277W>0.70_0.32as: True
F200W-F356W>1.00_0.32as: False
Kokorev+24_LRD_red1_0.32as: False
F150W-F200W<0.80_0.32as: False
F277W-F356W>0.60_0.32as: False
F277W-F444W>0.70_0.32as: False
Kokorev+24_LRD_red2_0.32as: False
Kokorev+24_LRD_0.32as: False
----------
****************************************

In the above cell, we have run the default EPOCHS implementation. We can, however, tweak a couple of options to allow lower redshift galaxies into the sample (by not enforcing a 2 sigma non-detection in the bluest band for every galaxy), or mask in a different instrument other than NIRCam (i.e. mask in “ACS_WFC+NIRCam”, for example).

Example 5: Selecting based on SED fitting properties

Not yet implemented!

We may wish to extract, for example, a stellar mass complete sample from the stellar masses of our galaxies as calculated by {insert your favourite SED fitting code here}.