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);targetsis 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_metricsto list registered names anddescribe_metrics(name)for details on a specific one.If a callable, it must accept two
pyfar.Coordinatesarguments(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
metricis a callable (the callable handles its own return shape). WhenTrueandmetricis 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 whoseoutput_unitis 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
UserWarningand are dropped; valid ones are forwarded. Seedescribe_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:
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
0and 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
nameinMETRIC_FUNCTIONS.- Return type:
callable
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 astrue.
- Returns:
Lateral SD in radians, or
np.nanif no estimations fall within the ±80° band.- Return type:
- 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 astrue.
- Returns:
Lateral RMS in radians, or
np.nanif no estimations fall within the ±60° band.- Return type:
- 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\)
cutoffare included.
- Returns:
Mean signed lateral error in radians (positive: rightward bias), or
np.nanif no targets fall within the band.- Return type:
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:
- 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\)
cutoffare included.
- Returns:
Mean signed polar error in radians (positive: upward bias), or
np.nanif no estimations fall within the band.- Return type:
- Polar bias (mean signed error) within \(\pm\)
- 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:
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)\).
The inliers are initialised with the responses in the correct hemifield.
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.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\)
cutoffenter the analysis. UnlikequerrMiddlebrooksandrmsPmedianlocal, 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 astrue.cutoff (float, default=π/6) – Lateral-angle threshold in radians; only targets with \(|\alpha| \le\)
cutoffare 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.nanfor 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.nanif either hemifield fails: fewer thann_mininliers, inliers that all share one target polar angle (slope undefined), or no convergence. Unaffected bydegrees=Trueinlocalization_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 bydegrees=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
maxiteris reached; that hemifield is thennp.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
arccosclip 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:
- Raises:
ValueError – If any target or estimation vector has zero length, in which case its direction is undefined.