weiren119's picture
Feat: app.py
34acdd0
raw
history blame
6.57 kB
#!/usr/bin/env python3
"""
Copyright (c) 2020, Carleton University Biomedical Informatics Collaboratory
This source code is licensed under the MIT license found in the
LICENSE file in the root directory of this source tree.
"""
from typing import List
import numpy as np
VALID_FREQUENCIES = [125, 250, 500, 750, 1000, 1500, 2000, 3000, 4000, 6000, 8000, 16000]
VALID_THRESHOLDS = list(range(-10, 135, 5))
THRESHOLDS = list(range(-10, 130, 10))
OCTAVE_FREQS_HZ = [125, 250, 500, 1000, 2000, 4000, 8000]
INTEROCTAVE_FREQS_HZ = [750, 1500, 3000, 6000]
OCTAVE_FREQS_KHZ = [0.125, 0.25, 0.5, 1, 2, 4, 8]
INTEROCTAVE_FREQS_KHZ = [0.750, 1.5, 3, 6]
def round_threshold(threshold: float) -> int:
"""Returns the nearest multiple of 5 for the threshold input.
Parameters
----------
threshold : float
The threshold snapped to the nearest multiple of 5 along the y-axis.
Returns
-------
float
A ``snapped`` threshold value.
"""
return VALID_THRESHOLDS[np.argmin([abs(threshold - t) for t in VALID_THRESHOLDS])]
def round_frequency(frequency: float) -> int:
"""Returns the nearest audiologically meaningful frequency.
Parameters
----------
frequency : float
The frequency to be snapped to the nearest clinically meaningful frequency.
Returns
-------
float
A ``snapped`` frequency value.
"""
return VALID_FREQUENCIES[np.argmin([abs(frequency - f) for f in VALID_FREQUENCIES])]
def round_frequency_bone(frequency: float, direction: str, epsilon: float = 0.15) -> int:
"""Returns the nearest audiologically meaningful frequency.
Parameters
----------
frequency : float
The frequency to be snapped to the nearest clinically meaningful frequency.
epsilon: float
Distance (in octaves) below which a frequency is considered to be
exactly on the nearest valid frequency. (default: 0.15 octaves)
direction: str
This parameter will influence the snapping behavior as some
audiologists draw bone conduction symbols next to the target frequency,
while other draw it right on it.
epsilon: float
The frequency will be snapped in to the nearest frequency in the
provided direction, unless the distance to the nearest
frequency is < ε (some small distance (IN OCTAVE UNITS), in which
case the frequency will be snapped to that value.
Eg:
1K 2K 1K 2K
| | | |
| > | will be snapped to > |
| | | |
but if the threshold fell directly (within a very small distance of
1.5K, it would be snapped to that.
1K 2K 1K 1.5K 2K
| | | |
| > | will be snapped to | > |
| | | |
because it is really close to 1.5 and the audiologist likely
intentionally meant to indicate 1.5K rather than 1K.
Note: ε is a tweakable parameter that can be optimized over the
dataset.
Returns
-------
float
A ``snapped`` frequency value.
"""
assert direction == "left" or direction == "right"
distances = [abs(frequency_to_octave(frequency) - frequency_to_octave(f)) for f in VALID_FREQUENCIES]
nearest_frequency_index = np.argmin(distances)
snapped = None
if distances[nearest_frequency_index] < epsilon:
snapped = VALID_FREQUENCIES[nearest_frequency_index]
elif direction == "left":
if VALID_FREQUENCIES[nearest_frequency_index] > frequency:
snapped = VALID_FREQUENCIES[nearest_frequency_index - 1] if nearest_frequency_index > 0 else VALID_FREQUENCIES[nearest_frequency_index]
else:
snapped = VALID_FREQUENCIES[nearest_frequency_index]
else:
if VALID_FREQUENCIES[nearest_frequency_index] > frequency:
snapped = VALID_FREQUENCIES[nearest_frequency_index]
else:
snapped = VALID_FREQUENCIES[nearest_frequency_index + 1] if nearest_frequency_index < len(VALID_FREQUENCIES) - 1 else VALID_FREQUENCIES[nearest_frequency_index]
return snapped
def frequency_to_octave(frequency: float) -> float:
"""Converts a frequency (in Hz) to an octave value (linear units).
By convention, the 0th octave is 125Hz.
Parameters
----------
frequency : float
The frequency (a positive real) to be converted to an octave value.
Returns
-------
float
The octave corresponding to the input frequency.
"""
return np.log(frequency / 125) / np.log(2)
def octave_to_frequency(octave: float) -> float:
"""Converts an octave to its corresponding frequency value (in Hz).
By convention, the 0th octave is 125Hz.
Parameters
----------
octave : float
The octave to put on a frequency scale.
Returns
-------
float
The frequency value corresponding to the octave.
"""
return 125 * 2 ** octave
def stringify_measurement(measurement: dict) -> str:
"""Returns a string describing the measurement type that is compatible
with the NIHL portal format.
eg. An air conduction threshold for the right ear with no masking
would yield the string `AIR_UNMASKED_RIGHT`.
Parameters
----------
measurement: dict
A dictionary describing a threshold. Should have the keys `ear`,
`conduction` and `masking`.
Returns
-------
str
The string describing the measurement type in the NIHL portal format.
"""
masking = "masked" if measurement["masking"] else "unmasked"
return f"{measurement['conduction']}_{masking}_{measurement['ear']}".upper()
def measurement_string_to_dict(measurement_type: str) -> dict:
"""Converts a measurement type string in the NIHL portal format into
a dictionary with the equivalent information for use with the digitizer.
eg. `AIR_UNMASKED_RIGHT` would be equivalent to the dictionary:
{`ear`: `right`, `conduction`: `air`, `masking`: False}
Parameters
----------
measurement: dict
A dictionary describing a threshold. Should have the keys `ear`,
`conduction` and `masking`.
Returns
-------
str
The string describing the measurement type in the NIHL portal format.
"""
return {
"ear": "left" if "LEFT" in measurement_type else "right",
"conduction": "air" if "AIR" in measurement_type else "bone",
"masking": False if "UNMASKED" in measurement_type else True
}