Catalogue Fitting

Synference has a method specifically for fitting catalogues of sources - fit_catalogue.

This is designed to be a flexible and fast way to fit your full catalogue.

It can handle:

  1. Transforming observations to match the expected model features.

  2. Prior predictive checks/outlier detection to remove sources that are unlikely to be well modelled.

  3. Optional imputation of missing data.

  4. Rapid posterior inference using trained model.

  5. Optional SED recovery (see this notebook for more details).

  6. Returning structured results.

[1]:
import os

import numpy as np
from IPython.display import display
from synthesizer import get_grids_dir
from unyt import Jy

from synference import SBI_Fitter, load_unc_model_from_hdf5, test_data_dir

print(get_grids_dir())

available_grids = os.listdir(get_grids_dir())
if "test_grid.hdf5" not in available_grids:
    cmd = f"synthesizer-download --test-grids --destination {get_grids_dir()}"
    os.system(cmd)

library_path = os.path.join(get_grids_dir(), "test_grid.hdf5")
/opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
  from .autonotebook import tqdm as notebook_tqdm
/home/runner/.local/share/Synthesizer/grids

We’ll use a subset of the JADES spectroscopic catalogue used in the synference paper for this example.

[2]:
from astropy.table import Table

cat = Table.read(f"{test_data_dir}/jades_spec_catalogue_subset.fits")

The first step is the same as our SED recovery example - we will load our trained model and the noise model used to train the model. We need the noise model to transform our observations to match the model features.

[3]:
library_path = (
    f"{test_data_dir}/grid_BPASS_Chab_DenseBasis_SFH_0.01_z_14_logN_2.7_Calzetti_v3_multinode.hdf5"  # noqa: E501
)

fitter = SBI_Fitter.load_saved_model(
    model_file=f"{test_data_dir}", library_path=library_path, device="cpu"
)

nm_path = f"{test_data_dir}/BPASS_DenseBasis_v4_final_nsf_0_params_empirical_noise_models.h5"
noise_models = load_unc_model_from_hdf5(nm_path)

fitter.feature_array_flags["empirical_noise_models"] = noise_models
2026-06-24 09:55:02,317 | synference | INFO     | Loaded model from /home/runner/.local/share/Synthesizer/data/synference/BPASS_DenseBasis_v4_final_nsf_0_posterior.pkl.
2026-06-24 09:55:02,318 | synference | INFO     | Device: cpu
2026-06-24 09:55:02,367 | synference | WARNING  | IndexError when trying to set train/test arrays.

Now we need to explain our catalogue to synference. We create a ‘conversion_dict’ dictionary which maps the feature names to columns in the table. The expected column names for flux are the SVO format filter names, e.g. JWST/NIRCam.F070W, or just F070W if it is unambiguous. For flux errors it is the same, but with ‘unc_’ prefixed, e.g. unc_JWST/NIRCam.F070W or unc_F070W. Any other features (e.g. redshift) should be mapped to the column name in the catalogue.

In this case our catalogue is almost in the correct format, so we just need to convert the band names to include the facility and instrument. We do this by getting the band names from the fitted model (fitter.feature_names) and replacing the relevant parts of the strings.

We then create our conversion dictionary to map from e.g. ‘unc_F200W’ to ‘unc_JWST/NIRCam.F200W’.

[4]:
def band_to_instrument(band):
    """Band name to instrument mapping."""
    if band in ["F435W", "F606W", "F775W", "F814W", "F850LP"]:
        return f"HST/ACS_WFC.{band}"
    return f"JWST/NIRCam.{band}"


bands = [
    i.split(".")[-1]
    for i in fitter.feature_names
    if not (i.startswith("unc_") or i.startswith("redshift"))
]

print(bands)

conversion_dict = {band: band_to_instrument(band) for band in bands}
conversion_dict.update({f"unc_{band}": f"unc_{band_to_instrument(band)}" for band in bands})
conversion_dict["redshift"] = "redshift"

conversion_dict
['F435W', 'F606W', 'F775W', 'F814W', 'F850LP', 'F090W', 'F115W', 'F150W', 'F200W', 'F277W', 'F335M', 'F356W', 'F410M', 'F444W']
[4]:
{'F435W': 'HST/ACS_WFC.F435W',
 'F606W': 'HST/ACS_WFC.F606W',
 'F775W': 'HST/ACS_WFC.F775W',
 'F814W': 'HST/ACS_WFC.F814W',
 'F850LP': 'HST/ACS_WFC.F850LP',
 'F090W': 'JWST/NIRCam.F090W',
 'F115W': 'JWST/NIRCam.F115W',
 'F150W': 'JWST/NIRCam.F150W',
 'F200W': 'JWST/NIRCam.F200W',
 'F277W': 'JWST/NIRCam.F277W',
 'F335M': 'JWST/NIRCam.F335M',
 'F356W': 'JWST/NIRCam.F356W',
 'F410M': 'JWST/NIRCam.F410M',
 'F444W': 'JWST/NIRCam.F444W',
 'unc_F435W': 'unc_HST/ACS_WFC.F435W',
 'unc_F606W': 'unc_HST/ACS_WFC.F606W',
 'unc_F775W': 'unc_HST/ACS_WFC.F775W',
 'unc_F814W': 'unc_HST/ACS_WFC.F814W',
 'unc_F850LP': 'unc_HST/ACS_WFC.F850LP',
 'unc_F090W': 'unc_JWST/NIRCam.F090W',
 'unc_F115W': 'unc_JWST/NIRCam.F115W',
 'unc_F150W': 'unc_JWST/NIRCam.F150W',
 'unc_F200W': 'unc_JWST/NIRCam.F200W',
 'unc_F277W': 'unc_JWST/NIRCam.F277W',
 'unc_F335M': 'unc_JWST/NIRCam.F335M',
 'unc_F356W': 'unc_JWST/NIRCam.F356W',
 'unc_F410M': 'unc_JWST/NIRCam.F410M',
 'unc_F444W': 'unc_JWST/NIRCam.F444W',
 'redshift': 'redshift'}

Now we can run the inference. We specify the catalogue table, the conversion dictionary, the input flux units (Jy), and then we have some choices.

We can choose to:

  1. Run predictive checks to remove outliers.

  2. Impute or remove missing data.

  3. Run posterior inference, setting the number of posterior samples to draw.

  4. Recover SEDs for each source.

[5]:
fitter.recreate_simulator_from_library(
    override_library_path=library_path, override_grid_path="test_grid.hdf5"
)

post_tab = fitter.fit_catalogue(
    cat,
    columns_to_feature_names=conversion_dict,
    flux_units=Jy,
    check_out_of_distribution=False,
    recover_SEDs=False,
    missing_data_flag=np.nan,
    num_samples=300,
    append_to_input=False,
)
2026-06-24 09:55:02,422 | synference | INFO     | Overriding internal library name from provided file path.
2026-06-24 09:55:02,718 | synference | WARNING  | Failed to load cosmology from HDF5. Using Planck18 instead.
---------------------------------------------------------------------------
TimeoutError                              Traceback (most recent call last)
File /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/urllib/request.py:1348, in AbstractHTTPHandler.do_open(self, http_class, req, **http_conn_args)
   1347 try:
-> 1348     h.request(req.get_method(), req.selector, req.data, headers,
   1349               encode_chunked=req.has_header('Transfer-encoding'))
   1350 except OSError as err: # timeout error

File /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/http/client.py:1303, in HTTPConnection.request(self, method, url, body, headers, encode_chunked)
   1302 """Send a complete request to the server."""
-> 1303 self._send_request(method, url, body, headers, encode_chunked)

File /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/http/client.py:1349, in HTTPConnection._send_request(self, method, url, body, headers, encode_chunked)
   1348     body = _encode(body, 'body')
-> 1349 self.endheaders(body, encode_chunked=encode_chunked)

File /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/http/client.py:1298, in HTTPConnection.endheaders(self, message_body, encode_chunked)
   1297     raise CannotSendHeader()
-> 1298 self._send_output(message_body, encode_chunked=encode_chunked)

File /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/http/client.py:1058, in HTTPConnection._send_output(self, message_body, encode_chunked)
   1057 del self._buffer[:]
-> 1058 self.send(msg)
   1060 if message_body is not None:
   1061
   1062     # create a consistent interface to message_body

File /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/http/client.py:996, in HTTPConnection.send(self, data)
    995 if self.auto_open:
--> 996     self.connect()
    997 else:

File /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/http/client.py:962, in HTTPConnection.connect(self)
    961 sys.audit("http.client.connect", self, self.host, self.port)
--> 962 self.sock = self._create_connection(
    963     (self.host,self.port), self.timeout, self.source_address)
    964 # Might fail in OSs that don't implement TCP_NODELAY

File /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/socket.py:857, in create_connection(address, timeout, source_address)
    856 try:
--> 857     raise err
    858 finally:
    859     # Break explicitly a reference cycle

File /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/socket.py:845, in create_connection(address, timeout, source_address)
    844     sock.bind(source_address)
--> 845 sock.connect(sa)
    846 # Break explicitly a reference cycle

TimeoutError: [Errno 110] Connection timed out

During handling of the above exception, another exception occurred:

URLError                                  Traceback (most recent call last)
File /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/site-packages/synthesizer/instruments/filters.py:2041, in Filter._make_svo_filter(self)
   2040 try:
-> 2041     with urllib.request.urlopen(self.svo_url) as f:
   2042         # Get the root of the XML tree
   2043         root = ElementTree.parse(f).getroot()

File /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/urllib/request.py:216, in urlopen(url, data, timeout, cafile, capath, cadefault, context)
    215     opener = _opener
--> 216 return opener.open(url, data, timeout)

File /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/urllib/request.py:519, in OpenerDirector.open(self, fullurl, data, timeout)
    518 sys.audit('urllib.Request', req.full_url, req.data, req.headers, req.get_method())
--> 519 response = self._open(req, data)
    521 # post-process response

File /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/urllib/request.py:536, in OpenerDirector._open(self, req, data)
    535 protocol = req.type
--> 536 result = self._call_chain(self.handle_open, protocol, protocol +
    537                           '_open', req)
    538 if result:

File /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/urllib/request.py:496, in OpenerDirector._call_chain(self, chain, kind, meth_name, *args)
    495 func = getattr(handler, meth_name)
--> 496 result = func(*args)
    497 if result is not None:

File /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/urllib/request.py:1377, in HTTPHandler.http_open(self, req)
   1376 def http_open(self, req):
-> 1377     return self.do_open(http.client.HTTPConnection, req)

File /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/urllib/request.py:1351, in AbstractHTTPHandler.do_open(self, http_class, req, **http_conn_args)
   1350 except OSError as err: # timeout error
-> 1351     raise URLError(err)
   1352 r = h.getresponse()

URLError: <urlopen error [Errno 110] Connection timed out>

During handling of the above exception, another exception occurred:

SVOInaccessible                           Traceback (most recent call last)
Cell In[5], line 1
----> 1 fitter.recreate_simulator_from_library(
      2     override_library_path=library_path, override_grid_path="test_grid.hdf5"
      3 )
      5 post_tab = fitter.fit_catalogue(
      6     cat,
      7     columns_to_feature_names=conversion_dict,
   (...)
     13     append_to_input=False,
     14 )

File /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/site-packages/synference/sbi_runner.py:5652, in SBI_Fitter.recreate_simulator_from_library(self, set_self, overwrite, override_library_path, override_grid_path, **kwargs)
   5649 default_kwargs.update(kwargs)
   5651 try:
-> 5652     simulator = GalaxySimulator.from_library(
   5653         library_path, override_synthesizer_grid_dir=override_grid_path, **default_kwargs
   5654     )
   5655 except ValueError as e:
   5656     logger.error(
   5657         "Could not recreate simulator from grid. This model"
   5658         " may not be compatible. A GalaxySimulator object can"
   5659         " be provided manually to recover the SED."
   5660     )

File /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/site-packages/synference/library.py:5562, in GalaxySimulator.from_library(cls, library_path, override_synthesizer_grid_dir, override_emission_model, **kwargs)
   5547 dict_create = dict(
   5548     sfh_model=sfh_model,
   5549     zdist_model=zdist_model,
   (...)
   5559     fixed_params=fixed_params,
   5560 )
   5561 dict_create.update(kwargs)
-> 5562 return cls(**dict_create)

File /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/site-packages/synference/library.py:5108, in GalaxySimulator.__init__(self, sfh_model, zdist_model, grid, instrument, emission_model, emission_model_key, emitter_params, cosmo, param_order, param_units, param_transforms, out_flux_unit, required_keys, extra_functions, normalize_method, output_type, include_phot_errors, depths, depth_sigma, noise_models, fixed_params, photometry_to_remove, ignore_params, ignore_scatter, return_type, device)
   5105 self.reported_unused = False
   5107 if len(photometry_to_remove) > 0:
-> 5108     self.update_photo_filters(
   5109         photometry_to_remove=photometry_to_remove, photometry_to_add=None
   5110     )
   5112 if noise_models is not None:
   5113     assert isinstance(noise_models, dict), (
   5114         f"Noise models must be a dictionary. Got {type(noise_models)} instead."
   5115     )

File /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/site-packages/synference/library.py:5224, in GalaxySimulator.update_photo_filters(self, photometry_to_remove, photometry_to_add)
   5221     if filter_code not in new_filters:
   5222         new_filters.append(filter_code)
-> 5224 self.instrument.filters = FilterCollection(filter_codes=new_filters)
   5225 logger.info(f"Updated filters: {self.instrument.filters.filter_codes}")

File /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/site-packages/synthesizer/utils/operation_timers.py:98, in timed.<locals>.decorator.<locals>.wrapped(*args, **kwargs)
     94 tic(timer_name)
     95 try:
     96     # Return the wrapped function result unchanged so the decorator
     97     # is transparent aside from its timing side effect.
---> 98     return func(*args, **kwargs)
     99 finally:
    100     # Always stop the timer, even if the wrapped function raises,
    101     # so the timing stack remains balanced.
    102     toc(timer_name)

File /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/site-packages/synthesizer/instruments/filters.py:297, in FilterCollection.__init__(self, filter_codes, tophat_dict, generic_dict, filters, path, new_lam, fill_gaps, verbose)
    295 # Let's make the filters
    296 if filter_codes is not None:
--> 297     self._include_svo_filters(filter_codes)
    298 if tophat_dict is not None:
    299     self._include_top_hat_filters(tophat_dict)

File /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/site-packages/synthesizer/instruments/filters.py:474, in FilterCollection._include_svo_filters(self, filter_codes)
    471 # Loop over the given filter codes
    472 for f in filter_codes:
    473     # Get filter from SVO
--> 474     _filter = Filter(f, new_lam=self.lam)
    476     # Store the filter and its code
    477     self.filters[_filter.filter_code] = _filter

File /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/site-packages/synthesizer/units.py:909, in accepts.<locals>.check_accepts.<locals>.wrapped(*args, **kwargs)
    906 finally:
    907     toc(f"accepts({func.__qualname__})")
--> 909 return func(*bound.args, **bound.kwargs)

File /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/site-packages/synthesizer/utils/operation_timers.py:98, in timed.<locals>.decorator.<locals>.wrapped(*args, **kwargs)
     94 tic(timer_name)
     95 try:
     96     # Return the wrapped function result unchanged so the decorator
     97     # is transparent aside from its timing side effect.
---> 98     return func(*args, **kwargs)
     99 finally:
    100     # Always stop the timer, even if the wrapped function raises,
    101     # so the timing stack remains balanced.
    102     toc(timer_name)

File /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/site-packages/synthesizer/instruments/filters.py:1770, in Filter.__init__(self, filter_code, transmission, lam_min, lam_max, lam_eff, lam_fwhm, new_lam, hdf)
   1768 # Is this an SVO filter?
   1769 elif "/" in filter_code and "." in filter_code:
-> 1770     self._make_svo_filter()
   1772 # Otherwise we haven't got a valid combination of inputs.
   1773 else:
   1774     raise exceptions.InconsistentArguments(
   1775         "Invalid combination of filter inputs. \n For a generic "
   1776         "filter provide a transmission and wavelength array. "
   (...)
   1781         "wavelength or an effective wavelength and FWHM."
   1782     )

File /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/site-packages/synthesizer/instruments/filters.py:2052, in Filter._make_svo_filter(self)
   2049         data = root.find(".//TABLEDATA")
   2051 except URLError:
-> 2052     raise exceptions.SVOInaccessible(
   2053         (
   2054             f"The SVO Database at {self.svo_url} "
   2055             "is not responding. Is it down?"
   2056         )
   2057     )
   2059 # Throw an error if we didn't find the filter.
   2060 if field is None:

SVOInaccessible: The SVO Database at http://svo2.cab.inta-csic.es/theory/fps/fps.php?ID=HST/ACS_WFC.F435W is not responding. Is it down?

We can look at the output table, which is also an astropy.table.Table.

[6]:
post_tab
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[6], line 1
----> 1 post_tab

NameError: name 'post_tab' is not defined

And we can plot the star-forming main sequence from the fitted catalogue.

[7]:
import matplotlib.pyplot as plt

plt.scatter(post_tab["log_mass_50"], post_tab["log_sfr_50"])
plt.xlabel("Stellar Mass (log M_sun)")
plt.ylabel("SFR (log M_sun/yr)")
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[7], line 3
      1 import matplotlib.pyplot as plt
----> 3 plt.scatter(post_tab["log_mass_50"], post_tab["log_sfr_50"])
      4 plt.xlabel("Stellar Mass (log M_sun)")
      5 plt.ylabel("SFR (log M_sun/yr)")

NameError: name 'post_tab' is not defined

You may notice some of rows and nans. This is because those rows had missing data that we chose not to impute, which can’t be handled by the SBI inference method.

We could set missing_data_mcmc to True to impute those missing values instead.

We can also recover and plot the SEDs, which we’ll do for the first few sources in the catalogue.

[8]:
post_tab, data = fitter.fit_catalogue(
    cat[:4],
    columns_to_feature_names=conversion_dict,
    flux_units=Jy,
    check_out_of_distribution=False,
    recover_SEDs=True,
    plot_SEDs=True,
    missing_data_flag=np.nan,
    num_samples=300,
    append_to_input=False,
)
WARNING {'true_flux_units': Jy, 'out_units': 'AB'} arguments will have no effect with this model. Input must be in Jy.
WARNING {'true_flux_units': Jy, 'out_units': 'AB'} arguments will have no effect with this model. Input must be in Jy.
WARNING {'true_flux_units': Jy, 'out_units': 'AB'} arguments will have no effect with this model. Input must be in Jy.
WARNING {'true_flux_units': Jy, 'out_units': 'AB'} arguments will have no effect with this model. Input must be in Jy.
WARNING {'true_flux_units': Jy, 'out_units': 'AB'} arguments will have no effect with this model. Input must be in Jy.
WARNING {'true_flux_units': Jy, 'out_units': 'AB'} arguments will have no effect with this model. Input must be in Jy.
WARNING {'true_flux_units': Jy, 'out_units': 'AB'} arguments will have no effect with this model. Input must be in Jy.
WARNING {'true_flux_units': Jy, 'out_units': 'AB'} arguments will have no effect with this model. Input must be in Jy.
WARNING {'true_flux_units': Jy, 'out_units': 'AB'} arguments will have no effect with this model. Input must be in Jy.
WARNING {'true_flux_units': Jy, 'out_units': 'AB'} arguments will have no effect with this model. Input must be in Jy.
WARNING {'true_flux_units': Jy, 'out_units': 'AB'} arguments will have no effect with this model. Input must be in Jy.
WARNING {'true_flux_units': Jy, 'out_units': 'AB'} arguments will have no effect with this model. Input must be in Jy.
WARNING {'true_flux_units': Jy, 'out_units': 'AB'} arguments will have no effect with this model. Input must be in Jy.
WARNING {'true_flux_units': Jy, 'out_units': 'AB'} arguments will have no effect with this model. Input must be in Jy.
5s per sample.
Sampling from posterior: 100%|██████████| 4/4 [00:00<00:00, 17.16it/s]
2026-06-24 09:57:20,854 | synference | INFO     | Obtained posterior samples.

Recovering SEDs from posterior samples...:   0%|          | 0/4 [00:00<?, ?it/s]
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
Cell In[8], line 1
----> 1 post_tab, data = fitter.fit_catalogue(
      2     cat[:4],
      3     columns_to_feature_names=conversion_dict,
      4     flux_units=Jy,
      5     check_out_of_distribution=False,
      6     recover_SEDs=True,
      7     plot_SEDs=True,
      8     missing_data_flag=np.nan,
      9     num_samples=300,
     10     append_to_input=False,
     11 )

File /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/site-packages/synference/sbi_runner.py:3348, in SBI_Fitter.fit_catalogue(self, observations, columns_to_feature_names, flux_units, missing_data_flag, quantiles, sample_method, sample_kwargs, num_samples, timeout_seconds_per_row, override_transformations, append_to_input, return_feature_array, recover_SEDs, plot_SEDs, check_out_of_distribution, simulator, outlier_methods, missing_data_mcmc, missing_data_mcmc_params, return_full_samples, log_times, use_temp_samples, **kwargs)
   3345     if "." not in param:
   3346         extra_parameters[param] = obs_i[list(self.feature_names).index(param)]
-> 3348 fnu_quantiles, wav, phot_fnu_draws, phot_wav, fig = self.recover_SED(
   3349     X_test=obs_i,
   3350     samples=samples_i,
   3351     num_samples=num_samples,
   3352     sample_method=sample_method,
   3353     sample_kwargs=sample_kwargs,
   3354     plot=plot_SEDs,
   3355     plot_name=f"{self.name}_SED_{pos}",
   3356     simulator=simulator,
   3357     extra_parameters=extra_parameters,
   3358     verbose=False,
   3359 )
   3360 try:
   3361     id = table["ID"][pos]

File /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/site-packages/synference/sbi_runner.py:5779, in SBI_Fitter.recover_SED(self, X_test, samples, num_samples, sample_method, sample_kwargs, posteriors, simulator, prior, plot, marginalized_parameters, extra_parameters, phot_unit, true_parameters, plot_name, plots_dir, sample_color, param_labels, plot_closest_draw_to, plot_sfh, plot_histograms, kde, save_plots, fig, ax, ax_sfh, verbose)
   5777 if simulator is None:
   5778     if not self.has_simulator:
-> 5779         self.recreate_simulator_from_library(set_self=True)
   5781     if not hasattr(self, "simulator"):
   5782         raise ValueError("Simulator must be provided or set in the object.")

File /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/site-packages/synference/sbi_runner.py:5652, in SBI_Fitter.recreate_simulator_from_library(self, set_self, overwrite, override_library_path, override_grid_path, **kwargs)
   5649 default_kwargs.update(kwargs)
   5651 try:
-> 5652     simulator = GalaxySimulator.from_library(
   5653         library_path, override_synthesizer_grid_dir=override_grid_path, **default_kwargs
   5654     )
   5655 except ValueError as e:
   5656     logger.error(
   5657         "Could not recreate simulator from grid. This model"
   5658         " may not be compatible. A GalaxySimulator object can"
   5659         " be provided manually to recover the SED."
   5660     )

File /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/site-packages/synference/library.py:5316, in GalaxySimulator.from_library(cls, library_path, override_synthesizer_grid_dir, override_emission_model, **kwargs)
   5314     grid_name = os.path.basename(grid_dir).replace(".hdf5", "").replace(".h5", "")
   5315     grid_dir = os.path.dirname(grid_dir)
-> 5316 grid = Grid(grid_name, grid_dir)  # new_lam=lam)
   5318 # Step 2. Make instrument
   5319 if model_group["Instrument"].attrs.get("instrument_type", None) is None:

File /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/site-packages/synthesizer/units.py:909, in accepts.<locals>.check_accepts.<locals>.wrapped(*args, **kwargs)
    906 finally:
    907     toc(f"accepts({func.__qualname__})")
--> 909 return func(*bound.args, **bound.kwargs)

File /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/site-packages/synthesizer/utils/operation_timers.py:98, in timed.<locals>.decorator.<locals>.wrapped(*args, **kwargs)
     94 tic(timer_name)
     95 try:
     96     # Return the wrapped function result unchanged so the decorator
     97     # is transparent aside from its timing side effect.
---> 98     return func(*args, **kwargs)
     99 finally:
    100     # Always stop the timer, even if the wrapped function raises,
    101     # so the timing stack remains balanced.
    102     toc(timer_name)

File /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/site-packages/synthesizer/grid.py:185, in Grid.__init__(self, grid_name, grid_dir, ignore_spectra, spectra_to_read, ignore_lines, new_lam, lam_lims, use_precision)
    183 self._extract_axes = []
    184 self._extract_axes_values = {}
--> 185 self._get_axes()
    187 # Read in the metadata
    188 self._weight_var = None

File /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/site-packages/synthesizer/grid.py:356, in Grid._get_axes(self)
    354 """Get the grid axes from the HDF5 file."""
    355 # Get basic info of the grid
--> 356 with h5py.File(self.grid_filename, "r") as hf:
    357     # Get list of axes
    358     axes = list(hf.attrs["axes"])
    360     # Set the values of each axis as an attribute
    361     # e.g. self.log10age == hdf["axes"]["log10age"]

File /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/site-packages/h5py/_hl/files.py:555, in File.__init__(self, name, mode, driver, libver, userblock_size, swmr, rdcc_nslots, rdcc_nbytes, rdcc_w0, track_order, fs_strategy, fs_persist, fs_threshold, fs_page_size, page_buf_size, min_meta_keep, min_raw_keep, locking, alignment_threshold, alignment_interval, meta_block_size, track_times, **kwds)
    546     fapl = make_fapl(driver, libver, rdcc_nslots, rdcc_nbytes, rdcc_w0,
    547                      locking, page_buf_size, min_meta_keep, min_raw_keep,
    548                      alignment_threshold=alignment_threshold,
    549                      alignment_interval=alignment_interval,
    550                      meta_block_size=meta_block_size,
    551                      **kwds)
    552     fcpl = make_fcpl(track_order=track_order, track_times=track_times,
    553                      fs_strategy=fs_strategy, fs_persist=fs_persist,
    554                      fs_threshold=fs_threshold, fs_page_size=fs_page_size)
--> 555     fid = make_fid(name, mode, userblock_size, fapl, fcpl, swmr=swmr)
    557 if isinstance(libver, tuple):
    558     self._libver = libver

File /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/site-packages/h5py/_hl/files.py:232, in make_fid(name, mode, userblock_size, fapl, fcpl, swmr)
    230     if swmr:
    231         flags |= h5f.ACC_SWMR_READ
--> 232     fid = h5f.open(name, flags, fapl=fapl)
    233 elif mode == 'r+':
    234     fid = h5f.open(name, h5f.ACC_RDWR, fapl=fapl)

File h5py/_objects.pyx:54, in h5py._objects.with_phil.wrapper()

File h5py/_objects.pyx:55, in h5py._objects.with_phil.wrapper()

File h5py/h5f.pyx:106, in h5py.h5f.open()

FileNotFoundError: [Errno 2] Unable to synchronously open file (unable to open file: name = '/cosma7/data/dp276/dc-harv3/work/grids/bpass-2.2.1-bin_chabrier03-0.1,300.0_cloudy-c23.01-sps.hdf5', errno = 2, error message = 'No such file or directory', flags = 0, o_flags = 0)

If you recover the SEDs then a nested dictionary with the SED posteriors are also returned. Initial key is the source index or ID, then within that there are keys for ‘wav’, ‘fnu_quantiles’, ‘wav’, ‘phot_wav’, ‘phot_fnu_draws’ and ‘fig’.

[9]:
data[1].keys()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[9], line 1
----> 1 data[1].keys()

NameError: name 'data' is not defined

Here are the SED plots:

[10]:
for key in data.keys():
    display(data[key]["fig"])
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[10], line 1
----> 1 for key in data.keys():
      2     display(data[key]["fig"])

NameError: name 'data' is not defined

We can check the feature array used for inference by setting return_feature_array=True.

[11]:
feature_array, mask = post_tab, data = fitter.fit_catalogue(
    cat,
    columns_to_feature_names=conversion_dict,
    flux_units=Jy,
    check_out_of_distribution=False,
    return_feature_array=True,
    missing_data_flag=np.nan,
    num_samples=300,
    append_to_input=False,
)

feature_array
WARNING {'true_flux_units': Jy, 'out_units': 'AB'} arguments will have no effect with this model. Input must be in Jy.
WARNING {'true_flux_units': Jy, 'out_units': 'AB'} arguments will have no effect with this model. Input must be in Jy.
WARNING {'true_flux_units': Jy, 'out_units': 'AB'} arguments will have no effect with this model. Input must be in Jy.
WARNING {'true_flux_units': Jy, 'out_units': 'AB'} arguments will have no effect with this model. Input must be in Jy.
WARNING {'true_flux_units': Jy, 'out_units': 'AB'} arguments will have no effect with this model. Input must be in Jy.
WARNING {'true_flux_units': Jy, 'out_units': 'AB'} arguments will have no effect with this model. Input must be in Jy.
WARNING {'true_flux_units': Jy, 'out_units': 'AB'} arguments will have no effect with this model. Input must be in Jy.
WARNING {'true_flux_units': Jy, 'out_units': 'AB'} arguments will have no effect with this model. Input must be in Jy.
WARNING {'true_flux_units': Jy, 'out_units': 'AB'} arguments will have no effect with this model. Input must be in Jy.
WARNING {'true_flux_units': Jy, 'out_units': 'AB'} arguments will have no effect with this model. Input must be in Jy.
WARNING {'true_flux_units': Jy, 'out_units': 'AB'} arguments will have no effect with this model. Input must be in Jy.
WARNING {'true_flux_units': Jy, 'out_units': 'AB'} arguments will have no effect with this model. Input must be in Jy.
WARNING {'true_flux_units': Jy, 'out_units': 'AB'} arguments will have no effect with this model. Input must be in Jy.
WARNING {'true_flux_units': Jy, 'out_units': 'AB'} arguments will have no effect with this model. Input must be in Jy.
2026-06-24 09:57:21,880 | synference | INFO     | Removing 54 observations with missing data.
[11]:
array([[2.4493113e+01, 2.4163765e+01, 2.3604200e+01, ..., 9.3294436e-04,
        8.9689222e-04, 1.0870000e+00],
       [2.5077990e+01, 2.4549706e+01, 2.3904078e+01, ..., 6.5881172e-03,
        7.1780500e-03, 6.9384003e-01],
       [2.4922672e+01, 2.4493999e+01, 2.3787994e+01, ..., 1.1117980e-03,
        8.8765030e-04, 1.5530000e+00],
       ...,
       [2.6450207e+01, 2.4133791e+01, 2.2739664e+01, ..., 3.6288964e-04,
        2.9280997e-04, 6.6799998e-01],
       [2.7736353e+01, 2.6571774e+01, 2.5916496e+01, ..., 4.7382890e-04,
        3.4990878e-04, 1.6912301e+00],
       [2.3816370e+01, 2.3439030e+01, 2.3062237e+01, ..., 6.9473550e-04,
        6.0235627e-04, 5.7800002e+00]], shape=(76, 29), dtype=float32)

We can also run prior predictive checks to identify outliers, by setting check_out_of_distribution=True. The available list of methods is any of those in PYOD (https://pyod.readthedocs.io/en/latest/). You can set them with the ‘outlier_methods’ argument as a list of strings.