Skip to content

Commit 8099784

Browse files
authored
Merge pull request #3844 from h-mayorquin/metric_system_units_docs
Add documentation to handle (physical) units of recording channels
2 parents 948716b + 47b1c35 commit 8099784

File tree

6 files changed

+226
-4
lines changed

6 files changed

+226
-4
lines changed

doc/how_to/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@ Guides on how to solve specific, short problems in SpikeInterface. Learn how to.
1717
drift_with_lfp
1818
auto_curation_training
1919
auto_curation_prediction
20+
physical_units
2021
customize_a_plot

doc/how_to/physical_units.rst

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
Working with physical units in SpikeInterface recordings
2+
========================================================
3+
4+
In neurophysiology recordings, data is often stored in raw ADC (Analog-to-Digital Converter) integer values but needs to be analyzed in physical units.
5+
For extracellular recordings, this is typically microvolts (µV), but some recording devices may use different physical units.
6+
SpikeInterface provides tools to handle both situations.
7+
8+
It's important to note that **most spike sorters work fine on raw digital (ADC) units** and scaling is not needed. Going a step further, some sorters, such as Kilosort 3, require their input to be in raw ADC units.
9+
The specific behavior however depends on the spike sorter, so it's important to understand the specific input requirements on a case per case basis.
10+
11+
Many preprocessing tools are also linear transformations, and if the ADC is implemented as a linear transformation which is fairly common, then the overall effect can be preserved.
12+
That is, **preprocessing steps can often be applied either before or after unit conversion without affecting the outcome.**. That being said, there are rough edges to this approach.
13+
preprocessing algorithms like filtering, whitening, centering, interpolation and common reference require casting to float within the pipeline. We advise users to experiment
14+
with different approaches to find the best one for their specific use case.
15+
16+
17+
Therefore, **it is usually safe to work in raw ADC integer values unless a specific tool or analysis requires physical units**.
18+
If you are interested in visualizations, comparability across devices, or outputs with interpretable physical scales (e.g., microvolts), converting to physical units is recommended.
19+
Otherwise, remaining in raw units can simplify processing and preserve performance.
20+
21+
Understanding Physical Units
22+
----------------------------
23+
24+
Most recording devices store data in ADC units (integers) to save space and preserve the raw data.
25+
To convert these values to physical units, two parameters are needed:
26+
27+
* **gain**: A multiplicative factor to scale the raw values
28+
* **offset**: An additive factor to shift the values
29+
30+
The conversion formula is:
31+
32+
.. code-block:: text
33+
34+
physical_value = raw_value * gain + offset
35+
36+
37+
Converting to Physical Units
38+
----------------------------
39+
40+
SpikeInterface provides two preprocessing classes for converting recordings to physical units. Both wrap the
41+
``RecordingExtractor`` class and ensures that the data is returned in physical units when calling `get_traces <https://spikeinterface.readthedocs.io/en/stable/api.html#spikeinterface.core.BaseRecording.get_traces>`_
42+
43+
1. ``scale_to_uV``: The primary function for extracellular recordings. SpikeInterface is centered around
44+
extracellular recordings, and this function is designed to convert the data to microvolts (µV).
45+
2. ``scale_to_physical_units``: A general function for any physical unit conversion. This will allow you to extract the data in any
46+
physical unit, not just microvolts. This is useful for other types of recordings, such as force measurements in Newtons but should be
47+
handled with care.
48+
49+
For most users working with extracellular recordings, ``scale_to_uV`` is the recommended choice if they want to work in physical units:
50+
51+
.. code-block:: python
52+
53+
from spikeinterface.extractors import read_intan
54+
from spikeinterface.preprocessing import scale_to_uV
55+
56+
# Load recording (data is in ADC units)
57+
recording = read_intan("path/to/file.rhs")
58+
59+
# Convert to microvolts
60+
recording_uv = scale_to_uV(recording)
61+
62+
For recordings with non-standard units (e.g., force measurements in Newtons), use ``scale_to_physical_units``:
63+
64+
.. code-block:: python
65+
66+
from spikeinterface.preprocessing import scale_to_physical_units
67+
68+
# Convert to physical units (whatever they may be)
69+
recording_physical = scale_to_physical_units(recording)
70+
71+
Both preprocessors automatically:
72+
73+
1. Detect the appropriate gain and offset from the recording properties
74+
2. Apply the conversion to all channels
75+
3. Update the recording properties to reflect that data is now in physical units
76+
77+
Setting Custom Physical Units
78+
-----------------------------
79+
80+
While most extractors automatically set the appropriate ``gain_to_uV`` and ``offset_to_uV`` values,
81+
there might be cases where you want to set custom physical units. In these cases, you can set
82+
the following properties:
83+
84+
* ``physical_unit``: The target physical unit (e.g., 'uV', 'mV', 'N')
85+
* ``gain_to_unit``: The gain to convert from raw values to the target unit
86+
* ``offset_to_unit``: The offset to convert from raw values to the target unit
87+
88+
You need to set these properties for every channel, which allows for the case when there are different gains and offsets on different channels. Here's an example:
89+
90+
.. code-block:: python
91+
92+
# Set custom physical units
93+
num_channels = recording.get_num_channels()
94+
values = ["volts"] * num_channels
95+
recording.set_property(key='physical_unit', values=values)
96+
97+
gain_values = [0.001] * num_channels # Convert from ADC to volts
98+
recording.set_property(key='gain_to_unit', values=gain_values) # Convert to volts
99+
100+
offset_values = [0] * num_channels # No offset
101+
recording.set_property(key='offset_to_unit', values=offset_values) # No offset
102+
103+
# Apply the conversion using scale_to_physical_units
104+
recording_physical = scale_to_physical_units(recording)
105+
106+
This approach gives you full control over the unit conversion process while maintaining
107+
compatibility with SpikeInterface's preprocessing pipeline.

src/spikeinterface/core/baserecording.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,15 @@ class BaseRecording(BaseRecordingSnippets):
2020
"""
2121

2222
_main_annotations = BaseRecordingSnippets._main_annotations + ["is_filtered"]
23-
_main_properties = ["group", "location", "gain_to_uV", "offset_to_uV"]
23+
_main_properties = [
24+
"group",
25+
"location",
26+
"gain_to_uV",
27+
"offset_to_uV",
28+
"gain_to_physical_unit",
29+
"offset_to_physical_unit",
30+
"physical_unit",
31+
]
2432
_main_features = [] # recording do not handle features
2533

2634
_skip_properties = [

src/spikeinterface/preprocessing/preprocessinglist.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525
CenterRecording,
2626
center,
2727
)
28+
from .scale import scale_to_uV, scale_to_physical_units
2829

29-
from .scale import scale_to_uV
3030

3131
from .whiten import WhitenRecording, whiten, compute_whitening_matrix
3232
from .rectify import RectifyRecording, rectify

src/spikeinterface/preprocessing/scale.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,70 @@
11
from __future__ import annotations
22

3+
import numpy as np
4+
35
from spikeinterface.preprocessing.basepreprocessor import BasePreprocessor
6+
from spikeinterface.preprocessing.normalize_scale import ScaleRecording
7+
from spikeinterface.core.core_tools import define_function_handling_dict_from_class
8+
9+
10+
class ScaleToPhysicalUnits(ScaleRecording):
11+
"""
12+
Scale raw traces to their physical units using gain_to_physical_unit and offset_to_physical_unit.
13+
14+
This preprocessor uses the channel-specific gain and offset information
15+
stored in the recording extractor to convert the raw traces to their physical units.
16+
Most commonly this will be microvolts (µV) for voltage recordings, but some extractors
17+
might use different physical units (e.g., Newtons for force measurements).
18+
19+
Parameters
20+
----------
21+
recording : BaseRecording
22+
The recording extractor to be scaled. The recording extractor must
23+
have gain_to_physical_unit and offset_to_physical_unit properties set.
24+
25+
Returns
26+
-------
27+
ScaleToPhysicalUnits
28+
The recording with traces scaled to physical units.
29+
30+
Raises
31+
------
32+
ValueError
33+
If the recording extractor does not have gain_to_physical_unit and offset_to_physical_unit properties.
34+
"""
35+
36+
name = "recording_in_physical_units"
37+
38+
def __init__(self, recording):
39+
if "gain_to_physical_unit" not in recording.get_property_keys():
40+
error_msg = (
41+
"Recording must have 'gain_to_physical_unit' property to convert to physical units. \n"
42+
"Set the gain using `recording.set_property(key='gain_to_physical_unit', values=values)`."
43+
)
44+
raise ValueError(error_msg)
45+
if "offset_to_physical_unit" not in recording.get_property_keys():
46+
error_msg = (
47+
"Recording must have 'offset_to_physical_unit' property to convert to physical units. \n"
48+
"Set the offset using `recording.set_property(key='offset_to_physical_unit', values=values)`."
49+
)
50+
raise ValueError(error_msg)
51+
52+
gain = recording.get_property("gain_to_physical_unit")
53+
offset = recording.get_property("offset_to_physical_unit")
54+
55+
# Initialize parent ScaleRecording with the gain and offset values
56+
ScaleRecording.__init__(self, recording, gain=gain, offset=offset, dtype="float32")
57+
58+
# Reset gain/offset since data is now in physical units
59+
self.set_property(key="gain_to_physical_unit", values=np.ones(recording.get_num_channels(), dtype="float32"))
60+
self.set_property(key="offset_to_physical_unit", values=np.zeros(recording.get_num_channels(), dtype="float32"))
61+
62+
# Also reset channel gains and offsets
63+
self.set_channel_gains(gains=1.0)
64+
self.set_channel_offsets(offsets=0.0)
65+
66+
67+
scale_to_physical_units = define_function_handling_dict_from_class(ScaleToPhysicalUnits, name="scale_to_physical_units")
468

569

670
def scale_to_uV(recording: BasePreprocessor) -> BasePreprocessor:

src/spikeinterface/preprocessing/tests/test_scaling.py

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import pytest
22
import numpy as np
3-
from spikeinterface.core.testing_tools import generate_recording
4-
from spikeinterface.preprocessing import scale_to_uV, CenterRecording
3+
from spikeinterface.core.generate import generate_recording
4+
from spikeinterface.preprocessing import scale_to_uV, CenterRecording, scale_to_physical_units
55

66

77
def test_scale_to_uV():
@@ -70,6 +70,48 @@ def test_scaling_in_preprocessing_chain():
7070
np.testing.assert_allclose(traces_scaled_with_preprocessor, traces_scaled_with_preprocessor_and_argument)
7171

7272

73+
def test_scale_to_physical_units():
74+
# Create a sample recording extractor with fake physical unit gains and offsets
75+
num_channels = 4
76+
sampling_frequency = 30_000.0
77+
durations = [1.0] # seconds
78+
recording = generate_recording(
79+
num_channels=num_channels,
80+
durations=durations,
81+
sampling_frequency=sampling_frequency,
82+
)
83+
84+
rng = np.random.default_rng(0)
85+
gains = rng.random(size=(num_channels)).astype(np.float32)
86+
offsets = rng.random(size=(num_channels)).astype(np.float32)
87+
88+
# Set physical unit gains/offsets instead of regular gains/offsets
89+
recording.set_property("gain_to_physical_unit", gains)
90+
recording.set_property("offset_to_physical_unit", offsets)
91+
92+
# Apply the preprocessor
93+
scaled_recording = scale_to_physical_units(recording=recording)
94+
95+
# Get raw traces and apply scaling manually
96+
raw_traces = recording.get_traces(segment_index=0)
97+
expected_traces = raw_traces * gains + offsets
98+
99+
# Get scaled traces
100+
scaled_traces = scaled_recording.get_traces(segment_index=0)
101+
102+
# Check if the traces are scaled correctly
103+
np.testing.assert_allclose(scaled_traces, expected_traces)
104+
105+
# Test for the error when recording doesn't have physical unit properties
106+
recording_no_gains = generate_recording(
107+
num_channels=num_channels,
108+
durations=durations,
109+
sampling_frequency=sampling_frequency,
110+
)
111+
with pytest.raises(ValueError):
112+
scale_to_physical_units(recording_no_gains)
113+
114+
73115
if __name__ == "__main__":
74116
test_scale_to_uV()
75117
test_scaling_in_preprocessing_chain()

0 commit comments

Comments
 (0)