Fit the Model to Your Own HRTF

This guide shows how to estimate listener-specific model parameters from measured sound-localisation responses. The output is a dictionary of fitted noise parameters — sigma_spectral, sigma_prior, and sigma_motor — that characterise that listener’s sensory precision and spatial expectations.

The fitting procedure follows a two-stage profile-likelihood approach (see Likelihood function for the equations):

  1. Stage 1 (lateral only): estimate motor noise \(\sigma_\mathrm{m}\) from lateral responses using ITD and ILD cues, which dominate horizontal localisation and are largely independent of spectral processing.

  2. Stage 2 (full sphere): fix \(\sigma_\mathrm{m}\) and fit \(\sigma_\mathrm{mon}\) (spectral noise) and \(\sigma_\mathrm{prior}\) (prior width) by maximising the full-sphere likelihood.

Note

sigma_itd (0.569) and sigma_ild (1.0 dB) are fixed at literature values throughout — they cannot be separated from \(\kappa_\mathrm{m}\) along the same spatial dimension. See Noise parameters for the identifiability analysis.

Prepare your data

You need:

  • A SOFA file for the listener’s HRTF.

  • A pandas.DataFrame of measured responses with columns azi_target, ele_target, azi_response, ele_response (all in degrees, spherical-elevation convention).

  • A pyfar.Coordinates object with the target directions.

    import pandas as pd
    import pyfar as pf
    import numpy as np

    obs_tbl = pd.read_csv(DATA_CSV)

    targets = obs_tbl[["azi_target", "ele_target"]].drop_duplicates()
    targets_coords = pf.Coordinates.from_spherical_elevation(
        np.deg2rad(targets["azi_target"].values),
        np.deg2rad(targets["ele_target"].values),
        np.ones(len(targets)),
    )

Run the two-stage fit

fit_listener runs both stages and returns a results dictionary.

    result = fit_listener(
        sofa_path=sofa_path,
        obs_tbl=obs_tbl,
        num_repetitions=1,
        targets_coords=targets_coords,
        interpolation_method="SHMAX",
    )

    print(f"sigma_motor   = {result['sigma_motor']:.2f} deg")
    print(f"sigma_spectral = {result['sigma_spectral']:.2f} dB")
    print(f"sigma_prior   = {result['sigma_prior']:.2f} deg")
    print(f"NLL           = {result['nll']:.2f}")

Note

Fitting a single listener takes roughly 5–15 minutes depending on the number of trials and Monte Carlo repetitions. Set num_repetitions=50 for a quick exploratory fit; use the default num_repetitions=200 for publishable results.

Inspect the result

The returned dictionary contains all fitted and fixed parameter values, timing information, and the final negative log-likelihood:

    print(result["sigma_itd"])    # 0.569 (ITD noise, fixed)
    print(result["sigma_ild"])    # 1.0   (ILD noise, fixed by default)
    print(result["sigma_spectral"])
    print(result["sigma_prior"])
    print(result["kappa_motor"])  # concentration form of sigma_motor
    print(result["n_trials"])     # number of responses used
    print(result["time_total"])   # wall-clock seconds

What to do next

Use the fitted parameters to simulate responses with Simulate Localization Responses, or compare fits across interpolation methods with Compare Interpolation Methods. Full parameter documentation is in fit_listener.