-
Notifications
You must be signed in to change notification settings - Fork 213
Add documentation to handle (physical) units of recording channels #3844
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
0f61068
ee6542b
3c81942
9f05e45
3bd19e9
2b10362
06c4aba
102507c
1748800
2a86ae3
0498e44
0752f30
5a37cc3
75fe09a
32841a3
87f8b63
b00d190
fe732c9
703c8ed
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
Working with physical units in SpikeInterface recordings | ||
======================================================== | ||
|
||
In neurophysiology recordings, data is often stored in raw ADC (Analog-to-Digital Converter) units but needs to be analyzed in physical units. | ||
For extracellular recordings, this is typically microvolts (µV), but some recording devices may use different physical units. | ||
SpikeInterface provides tools to handle both situations. | ||
|
||
It's important to note that **most spike sorters work fine on raw digital (ADC) units** and scaling is not needed. | ||
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. | ||
That is, **preprocessing steps can often be applied either before or after unit conversion without affecting the outcome.** | ||
|
||
Therefore, **it is usually safe to work in raw ADC units unless a specific tool or analysis requires physical units**. | ||
If you are interested in visualizations, comparability across devices, or outputs with interpretable physical scales (e.g., microvolts), converting to physical units is recommended. | ||
Otherwise, remaining in raw units can simplify processing and preserve performance. | ||
|
||
Understanding Physical Units | ||
---------------------------- | ||
|
||
Most recording devices store data in ADC units (integers) to save space and preserve the raw data. | ||
To convert these values to physical units, two parameters are needed: | ||
|
||
* **gain**: A multiplicative factor to scale the raw values | ||
* **offset**: An additive factor to shift the values | ||
|
||
The conversion formula is: | ||
|
||
.. code-block:: text | ||
|
||
physical_value = raw_value * gain + offset | ||
|
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe a note here saying that as we discussed above because this is a linear transformation we can do preprocessing etc without an issue. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you add a suggestion? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep will do. Have some experiments all day today, but my hope is tomorrow should be a little freer. |
||
Converting to Physical Units | ||
---------------------------- | ||
|
||
SpikeInterface provides two preprocessing classes for converting recordings to physical units. Both wrap the | ||
``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>`_ | ||
|
||
1. ``scale_to_uV``: The primary function for extracellular recordings. SpikeInterface is centered around | ||
extracellular recordings, and this function is designed to convert the data to microvolts (µV). Many plotting | ||
and analyzing functions in SpikeInterface expect data in microvolts, so this is the recommended approach for most users. | ||
2. ``scale_to_physical_units``: A general function for any physical unit conversion. This will allow you to extract the data in any | ||
physical unit, not just microvolts. This is useful for other types of recordings, such as force measurements in Newtons but should be | ||
handled with care. | ||
|
||
For most users working with extracellular recordings, ``scale_to_uV`` is the recommended choice: | ||
|
||
.. code-block:: python | ||
|
||
from spikeinterface.extractors import read_intan | ||
from spikeinterface.preprocessing import scale_to_uV | ||
|
||
# Load recording (data is in ADC units) | ||
recording = read_intan("path/to/file.rhs") | ||
|
||
# Convert to microvolts | ||
recording_uv = scale_to_uV(recording) | ||
|
||
For recordings with non-standard units (e.g., force measurements in Newtons), use ``scale_to_physical_units``: | ||
|
||
.. code-block:: python | ||
|
||
from spikeinterface.preprocessing import scale_to_physical_units | ||
|
||
# Convert to physical units (whatever they may be) | ||
recording_physical = scale_to_physical_units(recording) | ||
|
||
Both preprocessors automatically: | ||
|
||
1. Detect the appropriate gain and offset from the recording properties | ||
2. Apply the conversion to all channels | ||
3. Update the recording properties to reflect that data is now in physical units | ||
|
||
Setting Custom Physical Units | ||
--------------------------- | ||
|
||
While most extractors automatically set the appropriate ``gain_to_uV`` and ``offset_to_uV`` values, | ||
there might be cases where you want to set custom physical units. In these cases, you can set | ||
the following properties: | ||
|
||
* ``physical_unit``: The target physical unit (e.g., 'uV', 'mV', 'N') | ||
* ``gain_to_unit``: The gain to convert from raw values to the target unit | ||
* ``offset_to_unit``: The offset to convert from raw values to the target unit | ||
|
||
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: | ||
|
||
.. code-block:: python | ||
|
||
# Set custom physical units | ||
num_channels = recording.get_num_channels() | ||
values = ["volts"] * num_channels | ||
recording.set_property(key='physical_unit', value=values) | ||
|
||
gain_values = [0.001] * num_channels # Convert from ADC to volts | ||
recording.set_property(key='gain_to_unit', values=gain_values) # Convert to volts | ||
|
||
offset_values = [0] * num_channels # No offset | ||
recording.set_property(key='offset_to_unit', values=offset_values) # No offset | ||
|
||
# Apply the conversion using scale_to_physical_units | ||
recording_physical = scale_to_physical_units(recording) | ||
|
||
This approach gives you full control over the unit conversion process while maintaining | ||
compatibility with SpikeInterface's preprocessing pipeline. |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -1,6 +1,69 @@ | ||||||
from __future__ import annotations | ||||||
|
||||||
import numpy as np | ||||||
|
||||||
from spikeinterface.preprocessing.basepreprocessor import BasePreprocessor | ||||||
from spikeinterface.preprocessing.normalize_scale import ScaleRecording | ||||||
|
||||||
|
||||||
class ScaleToPhysicalUnits(ScaleRecording): | ||||||
""" | ||||||
Scale raw traces to their physical units using gain_to_physical_unit and offset_to_physical_unit. | ||||||
|
||||||
This preprocessor uses the channel-specific gain and offset information | ||||||
stored in the recording extractor to convert the raw traces to their physical units. | ||||||
Most commonly this will be microvolts (µV) for voltage recordings, but some extractors | ||||||
might use different physical units (e.g., Newtons for force measurements). | ||||||
|
||||||
Parameters | ||||||
---------- | ||||||
recording : BaseRecording | ||||||
The recording extractor to be scaled. The recording extractor must | ||||||
have gain_to_physical_unit and offset_to_physical_unit properties set. | ||||||
|
||||||
Returns | ||||||
------- | ||||||
ScaleToPhysicalUnits | ||||||
The recording with traces scaled to physical units. | ||||||
|
||||||
Raises | ||||||
------ | ||||||
ValueError | ||||||
If the recording extractor does not have gain_to_physical_unit and offset_to_physical_unit properties. | ||||||
""" | ||||||
|
||||||
name = "recording_in_physical_units" | ||||||
|
||||||
def __init__(self, recording): | ||||||
if "gain_to_physical_unit" not in recording.get_property_keys(): | ||||||
error_msg = ( | ||||||
"Recording must have 'gain_to_physical_unit' property to convert to physical units. \n" | ||||||
"Set the gain using `recording.set_property(key='gain_to_physical_unit', value=values)`." | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
) | ||||||
raise ValueError(error_msg) | ||||||
if "offset_to_physical_unit" not in recording.get_property_keys(): | ||||||
error_msg = ( | ||||||
"Recording must have 'offset_to_physical_unit' property to convert to physical units. \n" | ||||||
"Set the offset using `recording.set_property(key='offset_to_physical_unit', value=values)`." | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
) | ||||||
raise ValueError(error_msg) | ||||||
|
||||||
gain = recording.get_property("gain_to_physical_unit") | ||||||
offset = recording.get_property("offset_to_physical_unit") | ||||||
|
||||||
# Initialize parent ScaleRecording with the gain and offset values | ||||||
ScaleRecording.__init__(self, recording, gain=gain, offset=offset, dtype="float32") | ||||||
|
||||||
# Reset gain/offset since data is now in physical units | ||||||
self.set_property(key="gain_to_physical_unit", values=np.ones(recording.get_num_channels(), dtype="float32")) | ||||||
self.set_property(key="offset_to_physical_unit", values=np.zeros(recording.get_num_channels(), dtype="float32")) | ||||||
|
||||||
# Also reset channel gains and offsets | ||||||
self.set_channel_gains(gains=1.0) | ||||||
self.set_channel_offsets(offsets=0.0) | ||||||
|
||||||
|
||||||
scale_to_physical_units = ScaleToPhysicalUnits | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hello, could you change this to from spikeinterface.core.core_tools import define_function_handling_dict_from_class
scale_to_physical_units = define_function_handling_dict_from_class(ScaleToPhysicalUnits, name="scale_to_physical_units") then will works with dicts of recs |
||||||
|
||||||
|
||||||
def scale_to_uV(recording: BasePreprocessor) -> BasePreprocessor: | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We might want Alessio or Sam to comment on some of the internal tooling. I think the scaling is automatic for some of our stuff so we should make it clear if/when we do that. I just don't remember off the top of my head. If Alessio doesn't have the time to look this over I can doublecheck in the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it is better if you check it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is only missing part to move this forward?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I agree @alejoe91 could you comment here when you have a moment :)