Skip to content

feat: create nwb acquisition module#9

Open
arjunsridhar12345 wants to merge 30 commits into
devfrom
7-create-nwb-acquisition-processing-module
Open

feat: create nwb acquisition module#9
arjunsridhar12345 wants to merge 30 commits into
devfrom
7-create-nwb-acquisition-processing-module

Conversation

@arjunsridhar12345

@arjunsridhar12345 arjunsridhar12345 commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator

This PR attempts to address #7. Uses information from this issue which has which raw streams to use: #5. The acquisition module currently in the NWB has these 4 streams associated with behavior:

  • left_reward_delivery_time
  • right_reward_delivery_time
  • left_lick_time
  • right_lick_time.

It sets up an acquisition builder module that can then be mapped directly to NWB without the need to take on dependencies from pynwb.

An initial example notebook can be found here on how to access the object:

https://github.com/AllenNeuralDynamics/dynamic-foraging-processing/blob/7-create-nwb-acquisition-processing-module/examples/acquisition_builder_example.ipynb

UPDATE: Removed example notebook because it's redundant to the raw data loader. Renamed to go under an nwb module since this is will be primarily used for nwb packaging

Added the reward annotation and licks. For now, licks are only using ports from the Janelia board. Can update it later I think if the lickety split ones are also used.

The hardware times are used for the timestamps. For the reward annotation, need trial information which comes from the software events. Thus, the closest software time is found for a given hardware time, and then that is used to annotate the reward

@micahwoodard

Copy link
Copy Markdown

for right/left_reward_delivery_time, value will be earned, manual, or automatic. Earned will be if the trial_outcome.data.trial.is_auto_response_right is null. automatic will be if auto_response is true or false. Manual is a bit trickier bc its not aligned with a go cue. There will be a GiveManualWaterRight software event that has the software timestamps of when manual water was given. These software event timestamps will need to be correlated with the harp timestamps

@arjunsridhar12345 arjunsridhar12345 marked this pull request as ready for review June 24, 2026 01:33
except (KeyError, FileNotFoundError):
return pd.DataFrame({"data": []})

def get_lick_times(self, is_right: bool) -> np.ndarray:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Do we want to make this compatible for the LicketySplit boards too? The software can switch between either. Might make it easier in the future if boxes ever want to switch but would also not be that hard to add when we need it

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

yeah I was honestly being lazy about this. I think it should be straightforward to add and can do it as part of this PR.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

tried to address here: 38fecfc. Takes in a stream name to allow for allowing compatibility for both boards

each timestamp as a detected lick (``True``).
"""
lick_times = self.get_lick_times(is_right)
port_column = "DIPort1" if is_right else "DIPort0"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

would it be easier to pass the port string as an arg rather than the is_right flag? Then the port would be hardcoded only one place and if anything changed in configuration, it would be easier to change

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

yeah good call. I went back and forth with it but seems like having the port passed in will make it more flexible to changes

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

kind of related to above, tried to address here: 38fecfc.

for i, trial_index in enumerate(trial_indices_in_reward_times):
if i in manual_indices_in_reward_times:
annotated_rewards.append("manual")
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

this is kinda confusing me but i could be misunderstanding it. Why do you need to loop through the trial_indices_in_reward_times to annotate the manual times? It seems like you should be able to annotate manual_indices_in_reward_times directly with "manual" since manual water is completely independent from trials and multiple could happen within a trial.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

ah good catch! yeah this is not needed, and also uncovered that I was not adding the manual times to the nwb. Will remove this logic, that should make it easier to follow I think

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

the times were already there. tried to simplify the logic here 38fecfc

@micahwoodard micahwoodard left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Okay this looks good to me. Not sure about the licketysplit stuff but minor thing since no df rig is using licketysplits yet

.at(stream_name)
.load()
.data
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I think reading either janelia lickometer or LicketySplit would look more like

        if "HarpLickometerRight" in data:          
            licks = self.data["HarpLickometerRight"]['LickState']["Channel0"] == True
        else:           
            di_state = data['HarpBehavior']['DigitalInputState'][port]
            lick = di_state.loc[di_state == True]

but would also need something to check for HarpLickometerRight and HarpLickometerLeft

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