Metrics

Functions for computing localisation metrics and registering custom ones.

Localisation metrics

bayesian_listener.metrics.localization_error(targets, estimations, metric, auxiliary_output=False, degrees=False, **kwargs)[source]

Compute the localization error between two sets of coordinates using the specified metric.

Parameters:
  • targets (pyfar.Coordinates) – The target (reference) coordinates, shape (n_targets,).

  • estimations (pyfar.Coordinates) – The estimated coordinates to compare against. May have an extra repetitions dimension, i.e. shape (n_targets, repetitions); targets is broadcast accordingly and both are flattened to (n_targets * repetitions,) before the metric is applied.

  • metric (str or Callable) –

    The metric to use for error computation.

    • If a string, it must be a registered metric name. Use describe_metrics to list registered names and describe_metrics (name) for details on a specific one.

    • If a callable, it must accept two pyfar.Coordinates arguments (targets, estimations) as the first two positional arguments, plus any keyword arguments forwarded via **kwargs. The user is responsible for coordinate convention and units. The callable must return either a single float or a tuple (error_value, auxiliary_data).

  • auxiliary_output (bool, default=False) – Ignored when metric is a callable (the callable handles its own return shape). When True and metric is a registered string, returns the auxiliary output dict alongside the error value.

  • degrees (bool, default=False) – When True, convert the returned error value to degrees. Has no effect for metrics whose output_unit is already 'degrees' or 'percentage'.

  • **kwargs (dict, optional) –

    Forwarded to the metric function.

    • For registered metrics, kwargs are validated against the function signature. Unknown kwargs raise a UserWarning and are dropped; valid ones are forwarded. See describe_metrics (name) for the per-metric kwarg list.

    • For callables, kwargs are forwarded as-is with no validation.

Returns:

The computed localization error. If auxiliary_output is True, the output will be a tuple: (error_value, auxiliary_data_dict). If the metric function does not provide auxiliary data, auxiliary_data_dict will be an empty dictionary.

Return type:

float or tuple

Examples

Registered metric with extra kwarg:

>>> error = localization_error(targets, estimations,
...                            'accL_cutoff',
...                            cutoff=np.deg2rad(30))

Registered metric with auxiliary output:

>>> error, aux = localization_error(targets, estimations,
...                                 'querrMiddlebrooks',
...                                 auxiliary_output=True)
>>> print(error)
9.375
>>> print(aux)
{'confusion_count': 48, 'response_count': 512}

Custom callable with extra kwarg:

>>> def my_metric(targets, estimations, threshold=0.5):
...     ...
>>> error = localization_error(targets, estimations,
...                            my_metric,
...                            threshold=0.1)
bayesian_listener.metrics.wrap_polar_angle(angle_rad)[source]

Wrap polar (vertical) angles to \([-\pi/2, 3\pi/2)\).

The interaural-polar convention places the front pole at 0 and the rear pole at π; wrapping to [-π/2, 3π/2) keeps the upper hemisphere contiguous and simplifies front/back error computations.

Parameters:

angle_rad (float or numpy.ndarray) – Polar angle(s) in radians.

Returns:

Wrapped angle(s) in radians, same shape as angle_rad.

Return type:

float or numpy.ndarray

Metric registry

Custom metrics can be registered and queried via the decorator API.

bayesian_listener.metrics.register_metric(name, coord_convention, input_unit, output_unit=None, description=None, kwargs_description=None, **extra_metadata)[source]

Decorator to register a metric function with metadata.

Parameters:
  • name (str) – Name of the metric.

  • coord_convention (str) – Coordinate convention used (e.g., ‘horizontal-polar’).

  • input_unit (str) – Unit of the input data (e.g., ‘radians’).

  • output_unit (str, optional) – Unit of the output data (e.g., ‘radians’, ‘percentage’).

  • description (str, optional) – Description of the metric.

  • kwargs_description (dict, optional) – Dictionary describing extra keyword arguments expected by the metric function. Keys are argument names, values are descriptions.

  • **extra_metadata (dict) – Additional metadata to store.

Returns:

decorator – Decorator that wraps the target function and registers it under name in METRIC_FUNCTIONS.

Return type:

callable

bayesian_listener.metrics.get_metric_metadata(name)[source]

Retrieve metadata for a registered metric.

Parameters:

name (str) – Name of the metric.

Returns:

metadata – Metadata dictionary for the metric.

Return type:

dict

bayesian_listener.metrics.describe_metrics(name=None)[source]

Print descriptions of registered metrics.

Parameters:

name (str, optional) – Name of the metric to describe. If None, lists all metrics.

Built-in metrics

Lateral error

bayesian_listener.metrics.sdL(true, est)[source]

Lateral standard-deviation error within \(\pm 80^\circ\) lateral.

Returns the standard deviation (square root of variance) of the response–target lateral-angle difference, restricted to estimations whose lateral angle satisfies \(|\hat{\alpha}| \le 80^\circ\). See Middlebrooks[1] for the foundational definition.

Parameters:
  • true (numpy.ndarray) – Target directions in horizontal-polar convention with lateral angles in radians, shape (..., 3).

  • est (numpy.ndarray) – Estimated directions, same shape and convention as true.

Returns:

Lateral SD in radians, or np.nan if no estimations fall within the ±80° band.

Return type:

float

bayesian_listener.metrics.rmsL(true, est)[source]

Lateral RMS error within \(\pm 60^\circ\) lateral (Middlebrooks[1]).

Parameters:
  • true (numpy.ndarray) – Target directions, horizontal-polar with lateral angle in radians.

  • est (numpy.ndarray) – Estimated directions, same convention as true.

Returns:

Lateral RMS in radians, or np.nan if no estimations fall within the ±60° band.

Return type:

float

bayesian_listener.metrics.accL_cutoff(true, est, cutoff=3.141592653589793)[source]

Lateral bias (mean signed error) within \(\pm\) cutoff.

Parameters:
  • true (numpy.ndarray) – Target directions, horizontal-polar with lateral angle in radians.

  • est (numpy.ndarray) – Estimated directions.

  • cutoff (float, default=π) – Lateral-angle threshold in radians; only targets with \(|\alpha| \le\) cutoff are included.

Returns:

Mean signed lateral error in radians (positive: rightward bias), or np.nan if no targets fall within the band.

Return type:

float

Polar error

bayesian_listener.metrics.rmsPmedianlocal(true, est)[source]

Local RMS polar error within \(\pm 30^\circ\) lateral, excluding quadrant errors.

Restricted to estimations with lateral angle \(|\hat{\alpha}| \le 30^\circ\) and polar error \(|\Delta \beta| < 90^\circ\). Definition follows Middlebrooks[1].

Parameters:
  • true (numpy.ndarray) – Target directions, horizontal-polar with angles in radians.

  • est (numpy.ndarray) – Estimated directions.

Returns:

Local polar RMS in radians.

Return type:

float

Raises:

ValueError – If estimated lateral angles fall outside \([-\pi/2, \pi/2]\), if no estimations land in the central band, or if every central estimation has a polar error \(\ge 90^\circ\).

bayesian_listener.metrics.accP_cutoff(true, est, cutoff=np.float64(0.5235987755982988))[source]
Polar bias (mean signed error) within \(\pm\) cutoff

lateral (Middlebrooks[1]).

Parameters:
  • true (numpy.ndarray) – Target directions, horizontal-polar with angles in radians.

  • est (numpy.ndarray) – Estimated directions.

  • cutoff (float, default=π/6) – Lateral-angle threshold in radians; only estimations with \(|\hat{\alpha}| \le\) cutoff are included.

Returns:

Mean signed polar error in radians (positive: upward bias), or np.nan if no estimations fall within the band.

Return type:

float

bayesian_listener.metrics.gainP(true, est, cutoff=np.float64(0.5235987755982988), delta=np.float64(0.6981317007977318), n_min=5, maxiter=100)[source]

Polar gain from the selective iterative regression procedure.

The polar gain is the slope of the linear regression of response on target polar angle, computed separately for the front (\(-90^\circ\) to \(+90^\circ\)) and rear (\(+90^\circ\) to \(+270^\circ\)) hemifields with the selective iterative regression procedure (SIRP) described by Macpherson and Middlebrooks[2], and averaged across the two hemifields. A gain of 1 indicates veridical polar localisation, 0 indicates responses unrelated to the target polar angle.

For each hemifield:

  1. The pool contains all responses to the targets in that hemifield (target polar angle \(\le 90^\circ\) for front, \(\ge 90^\circ\) for rear), polar angles wrapped to \([-90^\circ, 270^\circ)\).

  2. The inliers are initialised with the responses in the correct hemifield.

  3. A line is fitted by least squares to the inliers, and the new inliers are all pool points whose residual, wrapped to \([-180^\circ, 180^\circ)\), is smaller than delta.

  4. Step 3 is repeated until the inlier set no longer changes; the gain is the slope of the fit on that converged set.

Only targets with \(|\alpha| \le\) cutoff enter the analysis. Unlike querrMiddlebrooks and rmsPmedianlocal, which select on the response lateral angle, this selection is on the target lateral angle. It reflects the stimulus design of Macpherson and Middlebrooks[2] (targets within \(30^\circ\) of the median plane) rather than the regression procedure itself.

Parameters:
  • true (numpy.ndarray) – Target directions, horizontal-polar with angles in radians.

  • est (numpy.ndarray) – Estimated directions, same convention as true.

  • cutoff (float, default=π/6) – Lateral-angle threshold in radians; only targets with \(|\alpha| \le\) cutoff are included.

  • delta (float, default=2π/9) – Criterion distance \(\Delta\) in radians (\(40^\circ\)).

  • n_min (int, default=5) – Minimum number of inliers per hemifield, checked at initialisation and at every iteration. Fewer inliers yield np.nan for that hemifield.

  • maxiter (int, default=100) – Maximum number of regressions per hemifield.

Returns:

  • gain (float) – Mean of the front and rear polar gains (unitless), or np.nan if either hemifield fails: fewer than n_min inliers, inliers that all share one target polar angle (slope undefined), or no convergence. Unaffected by degrees=True in localization_error.

  • aux (dict) – Mapping with keys:

    • 'gain_front', 'gain_rear' (float) — per-hemifield gains.

    • 'bias_front', 'bias_rear' (float) — per-hemifield intercepts in radians (not converted by degrees=True).

    • 'n_front', 'n_rear' (int) — size of the converged inlier set, or of the last set considered when the procedure stopped early.

    • 'converged_front', 'converged_rear' (bool) — whether the inlier set converged.

Warns:

UserWarning – If the inlier sets of a hemifield cycle or maxiter is reached; that hemifield is then np.nan.

Notes

Results can differ from AMT’s localizationerror(m, 'gainP') in a minority of cases: AMT fits the final line to the iteration with the most inliers, whereas this implementation, following Macpherson and Middlebrooks[2], uses the converged inlier set.

Global error

bayesian_listener.metrics.querrMiddlebrooks(true, est)[source]

Quadrant-error rate within \(\pm 30^\circ\) lateral (Middlebrooks[1]).

Counts the fraction of central-band estimations whose polar error satisfies \(|\Delta \beta| \ge 90^\circ\).

Parameters:
  • true (numpy.ndarray) – Target directions, horizontal-polar with angles in radians.

  • est (numpy.ndarray) – Estimated directions.

Returns:

  • qerr (float) – Quadrant-error rate as a percentage.

  • aux (dict) – Mapping with keys:

    • 'confusion_count' (int) — number of estimations with \(|\Delta \beta| \ge 90^\circ\).

    • 'response_count' (int) — total estimations within the central ±30° lateral band.

Raises:

ValueError – If estimated lateral angles fall outside \([-\pi/2, \pi/2]\), or if no estimations land in the central ±30° band.

bayesian_listener.metrics.angular_error(true, est)[source]

Mean great-circle angular error between target and estimation directions.

Computes \(\bar{\theta} = \frac{1}{N} \sum \arccos( \hat{\mathbf{t}}_i \cdot \hat{\mathbf{e}}_i)\) where \(\hat{\mathbf{x}} = \mathbf{x} / \lVert \mathbf{x} \rVert\), with the dot product clipped to \([-1, 1]\) for numerical safety.

Both inputs are normalised to unit length, so the result depends only on direction and is invariant to the source distance. This matters because HRTF datasets are measured at different radii (e.g. 1.5 m vs 3.0 m); using the raw dot product would scale it by \(r_{\text{true}} r_{\text{est}}\) and saturate the arccos clip for all but the largest angular errors.

Parameters:
  • true (numpy.ndarray) – Target directions in Cartesian coordinates, shape (..., 3); any non-zero norm is accepted.

  • est (numpy.ndarray) – Estimated directions, same shape and convention.

Returns:

Mean angular error in radians.

Return type:

float

Raises:

ValueError – If any target or estimation vector has zero length, in which case its direction is undefined.