Skip to content
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

Acquisition device refactor #122

Merged
merged 11 commits into from
Jan 15, 2021
Merged

Acquisition device refactor #122

merged 11 commits into from
Jan 15, 2021

Conversation

lawhead
Copy link
Collaborator

@lawhead lawhead commented Jan 14, 2021

Overview

Refactored the acquisition module to separate the concept of a device (ex. DSI-24 headset) and a connection method to that device (TCP or LSL).

Ticket

https://www.pivotaltracker.com/story/show/175322346

Contributions

  • Created additional parameters for acq_device and acq_connection_method.
  • Added DeviceSpec class and a module for loading a list of supported devices from configuration; refactored many classes to take a DeviceSpec parameter, including the fake data servers.
  • Created an enumeration for supported ConnectionMethods.
  • Refactored previous concept of Device -> Connector.
  • Refactored fake data servers to simplify their usage. Renamed data_server module to tcp_data_server.
  • Fixed all acquisition demos.
  • Refactored lsl_connector in preparation for future work with multiple sources.

Test

  • The new acquisition parameters should appear in the params form and be editable.
  • All acquisition module tests should run without error.
  • All acquisition demos should run correctly.
  • Data acquisition during an experiment should use the configured device and connection method parameters and should behave the same as before, including trigger calibration.

Documentaion

  • Updated the README in the acquisition module.

@lawhead lawhead requested a review from tab-cmd January 14, 2021 01:00
Copy link
Contributor

@tab-cmd tab-cmd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some suggestions, but works locally for me and I like the approach!

@@ -87,6 +90,9 @@ def __init__(self,
self._max_wait = 0.1 # for process loop

self.marker_writer = NullMarkerWriter()

# column in the acquisition data used to calculate offset.
self.offset_column = OFFSET_COLUMN_NAME
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might be more meaningful to call it a trigger column and require that it's present. We're going to refactor the way we handle offset correction in a future ticket.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I went back and forth on this one. I'll go ahead and rename it to trigger_column for clarity.

info = StreamInfo(stream_name,
"EEG", len(channels),
self.sample_hz,
log.debug("Starting LSL server for device: %s", device_spec.name)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use f strings here

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logging is actually the one place where python is optimized to use the old string formatting style and f-strings can lead to some inefficiencies: https://docs.python.org/3/howto/logging.html#optimization. In this particular case it's probably not a big deal, but some code linters will provide a warning.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't know that! I just assumed f strings are more performant.

"description": "Wearable Sensing DSI-VR300"
},
{
"name": "g.USBamp-1",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that we're registering our devices this way

description=config['description'])


SUPPORTED_DEVICES = {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's move this to the top of the file


def supported_devices() -> Dict[str, DeviceSpec]:
"""Returns the currently supported devices keyed by name."""
global SUPPORTED_DEVICES
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this call needed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should prefix SUPPORTED_DEVICES with an underscore. My intent is that it's a private implementation detail of the module and shouldn't be accessed directly.



# Initialize the SUPPORTED_DEVICES
load()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be called in init_acquistion or in some other fashion?

Copy link
Collaborator Author

@lawhead lawhead Jan 15, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll re-work this a bit to call load when SUPPORTED_DEVICES is first accessed. There is other code that uses this module, so I don't want to make that code always have to explicitly load devices, but I agree that this doesn't need to be done at import time.

@@ -46,49 +49,87 @@ def inlet_name(inlet) -> str:
return name.replace('-', '_')


class LslDevice(Device):
"""Driver for any device streaming data through the LabStreamingLayer lib.
def channel_names(stream_info: pylsl.StreamInfo) -> List[str]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems like these could be a part of the lsldevice class?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I originally had this as a class method, but it didn't reference any instance variables and I got a linter warning about it so I pulled it out into a utility function. It may be useful in other contexts when we are looking at multiple LSL sources.

@@ -231,8 +230,7 @@ def read_data(self):
if not marker.is_empty and timestamp >= marker.timestamp:
trg = marker.trg
log.debug(("Appending %s marker %s to sample at time %s; ",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use f strings

Comment on lines +205 to +206
data = [
mock_record(n_channels) + [0 if (i + 1) < trigger_at else 1]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could be useful to define a function to produce this or set it as a class value

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I still want to take a pass through the code and revisit mocked EEG data.

@lawhead lawhead merged commit ddbcf51 into 1.4.3 Jan 15, 2021
@tab-cmd tab-cmd mentioned this pull request Jan 20, 2021
4 tasks
@lawhead lawhead deleted the acq-device-refactor branch January 29, 2021 22:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants