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

Ambient data #777

Open
wants to merge 3 commits into
base: iblrigv8dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions iblrig/base_choice_world.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,10 @@ def __init__(self, *args, delay_mins: float = 0, **kwargs):
'Temperature_C': np.zeros(NTRIALS_INIT) * np.nan,
'AirPressure_mb': np.zeros(NTRIALS_INIT) * np.nan,
'RelativeHumidity': np.zeros(NTRIALS_INIT) * np.nan,
}
},
dtype=np.float16,
)
self.ambient_sensor_table.rename_axis('Trial', inplace=True)

@staticmethod
def extra_parser():
Expand Down Expand Up @@ -218,7 +220,6 @@ def _run(self):

# save trial and update log
self.trial_completed(self.bpod.session.current_trial.export())
self.ambient_sensor_table.loc[i] = self.bpod.get_ambient_sensor_reading()
self.show_trial_log()

# handle stop event
Expand Down Expand Up @@ -526,6 +527,15 @@ def trial_completed(self, bpod_data: dict[str, Any]) -> None:
self.session_info.NTRIALS += 1
# SAVE TRIAL DATA
self.save_trial_data_to_json(bpod_data)

# save ambient data
if self.hardware_settings.device_bpod.USE_AMBIENT_MODULE:
sensor_reading = self.bpod.get_ambient_sensor_reading()
self.ambient_sensor_table.iloc[self.trial_num] = sensor_reading
self.bpod.write_ambient_data(
Copy link
Member

Choose a reason for hiding this comment

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

Yes so here it is worthwhile noting that we get to open/close the file at each trial.
It is not an issue in our case, but for a much faster stream, we would rather open the file at the start of the session, and feed the file pointer to this method.

The idea being that the method can parse the type being fed, if it is a file pointer it writes directly, if it is a file path it opens / closes the file as usual.

filepath=self.paths['AMBIENT_FILE_PATH'], trial_number=self.trial_num, sensor_reading=sensor_reading
)

# this is a flag for the online plots. If online plots were in pyqt5, there is a file watcher functionality
Path(self.paths['DATA_FILE_PATH']).parent.joinpath('new_trial.flag').touch()
self.paths.SESSION_FOLDER.joinpath('transfer_me.flag').touch()
Expand Down
3 changes: 3 additions & 0 deletions iblrig/base_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,8 @@ def _init_paths(self, append: bool = False) -> Bunch:
`C:\iblrigv8_data\mainenlab\Subjects\SWC_043\2019-01-01\001\raw_task_data_00`
* DATA_FILE_PATH: contains the bpod trials
`C:\iblrigv8_data\mainenlab\Subjects\SWC_043\2019-01-01\001\raw_task_data_00\_iblrig_taskData.raw.jsonable`
* AMBIENT_FILE_PATH: contains the ambient sensor data
`C:\iblrigv8_data\mainenlab\Subjects\SWC_043\2019-01-01\001\raw_task_data_00\_iblrig_ambientSensorData.raw.bin`
* SETTINGS_FILE_PATH: contains the task settings
`C:\iblrigv8_data\mainenlab\Subjects\SWC_043\2019-01-01\001\raw_task_data_00\_iblrig_taskSettings.raw.json`
"""
Expand Down Expand Up @@ -310,6 +312,7 @@ def _init_paths(self, append: bool = False) -> Bunch:
self.session_info.SESSION_NUMBER = int(paths.SESSION_FOLDER.name)
paths.SESSION_RAW_DATA_FOLDER = paths.SESSION_FOLDER.joinpath(paths.TASK_COLLECTION)
paths.DATA_FILE_PATH = paths.SESSION_RAW_DATA_FOLDER.joinpath('_iblrig_taskData.raw.jsonable')
paths.AMBIENT_FILE_PATH = paths.SESSION_RAW_DATA_FOLDER.joinpath('_iblrig_ambientSensorData.raw.bin')
paths.SETTINGS_FILE_PATH = paths.SESSION_RAW_DATA_FOLDER.joinpath('_iblrig_taskSettings.raw.json')
return paths

Expand Down
83 changes: 67 additions & 16 deletions iblrig/hardware.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from typing import Annotated, Literal

import numpy as np
import pandas as pd
import serial
import sounddevice as sd
from annotated_types import Ge, Le
Expand All @@ -30,6 +31,10 @@
from pybpodapi.state_machine import StateMachine

SOFTCODE = IntEnum('SOFTCODE', ['STOP_SOUND', 'PLAY_TONE', 'PLAY_NOISE', 'TRIGGER_CAMERA'])
DTYPE_AMBIENT_SENSOR_RAW = np.dtype(
[('Temperature_C', np.float16), ('AirPressure_mb', np.float16), ('RelativeHumidity', np.float16)]
)
DTYPE_AMBIENT_SENSOR_BIN = np.dtype([('Trial', np.uint16)] + DTYPE_AMBIENT_SENSOR_RAW.descr)

# some annotated types
Uint8 = Annotated[int, Ge(0), Le(255)]
Expand Down Expand Up @@ -206,23 +211,69 @@ def define_rotary_encoder_actions(self, module: BpodModule | None = None) -> Non
}
)

def get_ambient_sensor_reading(self):
def get_ambient_sensor_reading(self) -> np.ndarray:
"""
Retrieve ambient sensor readings.

If the ambient sensor module is not available, returns an array filled with NaN values.
Otherwise, retrieves the temperature, air pressure, and relative humidity readings.

Returns
-------
np.ndarray
A NumPy array containing the sensor readings in the following order:

- [0] : Temperature in degrees Celsius
- [1] : Air pressure in millibars
- [2] : Relative humidity in percentage
"""
if self.ambient_module is None:
return {
'Temperature_C': np.nan,
'AirPressure_mb': np.nan,
'RelativeHumidity': np.nan,
}
self.ambient_module.start_module_relay()
self.bpod_modules.module_write(self.ambient_module, 'R')
reply = self.bpod_modules.module_read(self.ambient_module, 12)
self.ambient_module.stop_module_relay()

return {
'Temperature_C': np.frombuffer(bytes(reply[:4]), np.float32)[0],
'AirPressure_mb': np.frombuffer(bytes(reply[4:8]), np.float32)[0] / 100,
'RelativeHumidity': np.frombuffer(bytes(reply[8:]), np.float32)[0],
}
data = np.full(3, np.nan, np.float16)
else:
self.ambient_module.start_module_relay()
self.bpod_modules.module_write(self.ambient_module, 'R')
reply = self.bpod_modules.module_read(self.ambient_module, 12)
self.ambient_module.stop_module_relay()
data = np.frombuffer(bytes(reply), dtype=np.float16).copy()
Copy link
Member

Choose a reason for hiding this comment

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

Is the original data in float32 ? Why do we use float16 here ?
I'm not worried about precision but float16 is not straightforward to manipulate and we reserve the use to very large datasets. I would write float32 for user side convenience.

data[1] /= 100
return data

@staticmethod
def write_ambient_data(filepath: Path | str, trial_number: int, sensor_reading: np.ndarray):
"""
Write ambient sensor data to a binary file.

Parameters
----------
filepath : Path or str
The path to the file where the data will be written.
trial_number : int
The trial number associated with the sensor readings.
sensor_reading : np.ndarray
A 1D array containing three sensor readings: temperature, air pressure, and relative humidity.
"""
with Path(filepath).open('ab') as f:
ambient_data = np.array([(trial_number, *sensor_reading)], dtype=DTYPE_AMBIENT_SENSOR_BIN)
ambient_data.tofile(f)

@staticmethod
def read_ambient_data(filepath: Path | str) -> pd.DataFrame:
Copy link
Member

Choose a reason for hiding this comment

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

I would put this with the jsonable in iblutils !

"""
Read ambient sensor data from a binary file into a DataFrame.

Parameters
----------
filepath : Path or str
The path to the file from which the data will be read.

Returns
-------
pd.DataFrame
A DataFrame containing the ambient sensor data, with columns corresponding to the fields
defined in `DTYPE_AMBIENT_SENSOR_BIN`.
"""
data = np.fromfile(filepath, dtype=DTYPE_AMBIENT_SENSOR_BIN)
return pd.DataFrame(data)

def flush(self):
"""Flushes valve 1."""
Expand Down
Loading