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

create physioevents.tsv.gz file #76

Closed
wants to merge 24 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b6d0225
modify converter and tests according to issues (#57) and (#60)
julia-pfarr Apr 10, 2024
c462c0a
tmp
Remi-Gau Apr 6, 2024
3ef8156
[pre-commit.ci] pre-commit autoupdate (#65)
pre-commit-ci[bot] Apr 9, 2024
fb37409
Merge branch 'updated-spec_julia' of github.com:bids-standard/eye2bid…
julia-pfarr Apr 10, 2024
fe7df43
ignore test ipynb
julia-pfarr Apr 10, 2024
9244429
Delete eye2bids/2eyes.ipynb
julia-pfarr Apr 10, 2024
d5bef7f
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 10, 2024
b5f971a
change how json dicts are combined because tests were failing
julia-pfarr Apr 11, 2024
809e192
Merge branch 'updated-spec_julia' of github.com:bids-standard/eye2bid…
julia-pfarr Apr 11, 2024
56cca43
ignore *ipynb
julia-pfarr Apr 11, 2024
f79d237
add PhysioType metadata
julia-pfarr Apr 18, 2024
a252a5d
Merge github.com:bids-standard/eye2bids into messages
julia-pfarr May 13, 2024
e0dd90e
resolve merge residuals
julia-pfarr May 13, 2024
a4e72ee
Merge branch 'main' into messages
julia-pfarr Jun 4, 2024
3490d8d
first try for physioevents.tsv.gz
julia-pfarr Jun 5, 2024
baae3c0
save pre-final physioevents.tsv.gz code
julia-pfarr Jun 5, 2024
0277a79
add tests for physioevents.tsv
julia-pfarr Jun 14, 2024
af2b24d
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 14, 2024
3e35540
fix error in tests
julia-pfarr Jun 14, 2024
61b3908
merge
julia-pfarr Jun 14, 2024
06659fe
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 14, 2024
3e06d52
try fix test
julia-pfarr Jun 14, 2024
807efee
merge
julia-pfarr Jun 14, 2024
d3a6468
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 14, 2024
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ eye2bids/_version.py
*events.json
*eyetrack.json
*eyetrack.tsv
*.ipynb
2eyes.ipynb
tmp

Expand Down
177 changes: 170 additions & 7 deletions eye2bids/edf2bids.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import gzip
import json
import re
import subprocess
from pathlib import Path

Expand Down Expand Up @@ -323,6 +324,111 @@ def _load_asc_file_as_reduced_df(events_asc_file: str | Path) -> pd.DataFrame:
return pd.DataFrame(df_ms.iloc[0:, 2:])


def _df_events_after_start(events: list) -> pd.DataFrame:

start_index = next(
i for i, line in enumerate(events) if re.match(r"START\s+.*", line)
)
end_index = next(
i for i in range(len(events) - 1, -1, -1) if re.match(r"END\s+.*", events[i])
)

if end_index > start_index:
data_lines = events[start_index + 1 : end_index]
return pd.DataFrame([line.strip().split("\t") for line in data_lines])
else:
return print("No 'END' found after the selected 'START'.")


def _df_physioevents(events_after_start: pd.DataFrame) -> pd.DataFrame:
events_after_start["Event_Letters"] = (
events_after_start[0].str.extractall(r"([A-Za-z]+)").groupby(level=0).agg("".join)
)
events_after_start["Event_Numbers"] = events_after_start[0].str.extract(r"(\d+)")
events_after_start[["msg_timestamp", "message"]] = events_after_start[1].str.split(
n=1, expand=True
)
events_after_start["message"] = events_after_start["message"].astype(str)

msg_mask = events_after_start["Event_Letters"] == "MSG"
events_after_start.loc[msg_mask, "Event_Numbers"] = events_after_start.loc[
msg_mask, "msg_timestamp"
]
physioevents_reordered = (
pd.concat(
[
events_after_start["Event_Numbers"],
events_after_start[2],
events_after_start["Event_Letters"],
events_after_start["message"],
],
axis=1,
ignore_index=True,
)
.replace({None: np.nan, "None": np.nan})
.rename(columns={0: "timestamp", 1: "duration", 2: "trial_type", 3: "message"})
)
return physioevents_reordered


def _physioevents_eye1(physioevents_reordered: pd.DataFrame) -> pd.DataFrame:
physioevents_eye1_list = ["MSG", "EFIXL", "ESACCL", "EBLINKL"]
physioevents_eye1 = physioevents_reordered[
physioevents_reordered["trial_type"].isin(physioevents_eye1_list)
]
physioevents_eye1 = physioevents_eye1.replace(
{"EFIXL": "fixation", "ESACCL": "saccade", "MSG": np.nan, None: np.nan}
)

physioevents_eye1["blink"] = 0
last_non_na_trial_type = None

for i in range(len(physioevents_eye1)):
current_trial_type = physioevents_eye1.iloc[i]["trial_type"]
if pd.notna(current_trial_type):
if current_trial_type == "saccade" and last_non_na_trial_type == "EBLINKL":
physioevents_eye1.iloc[i, physioevents_eye1.columns.get_loc("blink")] = 1
last_non_na_trial_type = current_trial_type

physioevents_eye1.loc[physioevents_eye1["trial_type"].isna(), "blink"] = np.nan
physioevents_eye1["blink"] = physioevents_eye1["blink"].astype("object")
physioevents_eye1 = physioevents_eye1[physioevents_eye1.trial_type != "EBLINKL"]

physioevents_eye1 = physioevents_eye1[
["timestamp", "duration", "trial_type", "blink", "message"]
]
return physioevents_eye1


def _physioevents_eye2(physioevents_reordered: pd.DataFrame) -> pd.DataFrame:
physioevents_eye2_list = ["MSG", "EFIXR", "ESACCR", "EBLINKR"]
physioevents_eye2 = physioevents_reordered[
physioevents_reordered["trial_type"].isin(physioevents_eye2_list)
]
physioevents_eye2 = physioevents_eye2.replace(
{"EFIXR": "fixation", "ESACCR": "saccade", "MSG": np.nan, None: np.nan}
)

physioevents_eye2["blink"] = 0
last_non_na_trial_type = None

for i in range(len(physioevents_eye2)):
current_trial_type = physioevents_eye2.iloc[i]["trial_type"]
if pd.notna(current_trial_type):
if current_trial_type == "saccade" and last_non_na_trial_type == "EBLINKR":
physioevents_eye2.iloc[i, physioevents_eye2.columns.get_loc("blink")] = 1
last_non_na_trial_type = current_trial_type

physioevents_eye2.loc[physioevents_eye2["trial_type"].isna(), "blink"] = np.nan
physioevents_eye2["blink"] = physioevents_eye2["blink"].astype("object")
physioevents_eye2 = physioevents_eye2[physioevents_eye2.trial_type != "EBLINKR"]

physioevents_eye2 = physioevents_eye2[
["timestamp", "duration", "trial_type", "blink", "message"]
]
return physioevents_eye2


def edf2bids(
input_file: str | Path | None = None,
metadata_file: str | Path | None = None,
Expand Down Expand Up @@ -358,10 +464,8 @@ def edf2bids(

# eye-physio.json Metadata
base_json = {
"PhysioType": "eyetrack",
"Columns": ["x_coordinate", "y_coordinate", "pupil_size", "timestamp"],
"timestamp": {
"Description": "Timestamp issued by the eye-tracker indexing the continuous recordings corresponding to the sampled eye."
},
"x_coordinate": {
"Description": "Gaze position x-coordinate of the recorded eye, in the coordinate units specified in the corresponding metadata sidecar.",
"Units": "a.u.",
Expand All @@ -374,11 +478,14 @@ def edf2bids(
"Description": "Pupil area of the recorded eye as calculated by the eye-tracker in arbitrary units (see EyeLink's documentation for conversion).",
"Units": "a.u.",
},
"timestamp": {
"Description": "Timestamp issued by the eye-tracker indexing the continuous recordings corresponding to the sampled eye."
},
"Manufacturer": "SR-Research",
"ManufacturersModelName": _extract_ManufacturersModelName(events),
"DeviceSerialNumber": _extract_DeviceSerialNumber(events),
"EnvironmentCoordinates": metadata.get("EnvironmentCoordinates"),
"SoftwareVersion": metadata.get("SoftwareVersion"),
"SoftwareVersions": metadata.get("SoftwareVersion"),
"EyeCameraSettings": metadata.get("EyeCameraSettings"),
"EyeTrackerDistance": metadata.get("EyeTrackerDistance"),
"FeatureDetectionSettings": metadata.get("FeatureDetectionSettings"),
Expand Down Expand Up @@ -503,13 +610,13 @@ def edf2bids(

samples = pd.read_csv(samples_asc_file, sep="\t", header=None)
samples_eye1 = (
pd.DataFrame(samples.iloc[:, 0:4])
pd.DataFrame(samples.iloc[:, [2, 1, 3, 0]])
.map(lambda x: x.strip() if isinstance(x, str) else x)
.replace(".", np.nan, regex=False)
)

if _2eyesmode(df_ms_reduced) == True:
samples_eye2 = pd.DataFrame(samples.iloc[:, [0, 4, 5, 6]])
samples_eye2 = pd.DataFrame(samples.iloc[:, [4, 5, 6, 0]])

# Samples to eye_physio.tsv.gz

Expand Down Expand Up @@ -539,7 +646,63 @@ def edf2bids(

e2b_log.info(f"file generated: {output_filename_eye2}")

# Messages and events to physioevents.tsv.gz - tbc
# Messages and events to dataframes

events_after_start = _df_events_after_start(events)
physioevents_reordered = _df_physioevents(events_after_start)
physioevents_eye1 = _physioevents_eye1(physioevents_reordered)
physioevents_eye2 = _physioevents_eye2(physioevents_reordered)

# Messages and events to physioevents.tsv.gz

if _2eyesmode(df_ms_reduced) == False:
output_eventsfilename_eye1 = generate_output_filename(
output_dir=output_dir,
input_file=input_file,
suffix="_recording-eye1_physioevents",
extension="tsv.gz",
)
if _extract_RecordedEye(df_ms_reduced) == "Left":
content = physioevents_eye1.to_csv(
sep="\t", index=False, na_rep="n/a", header=None
)
elif _extract_RecordedEye(df_ms_reduced) == "Right":
content = physioevents_eye2.to_csv(
sep="\t", index=False, na_rep="n/a", header=None
)
with gzip.open(output_eventsfilename_eye1, "wb") as f:
f.write(content.encode())

e2b_log.info(f"file generated: {output_eventsfilename_eye1}")

else:
output_eventsfilename_eye1 = generate_output_filename(
output_dir=output_dir,
input_file=input_file,
suffix="_recording-eye1_physioevents",
extension="tsv.gz",
)
content = physioevents_eye1.to_csv(
sep="\t", index=False, na_rep="n/a", header=None
)
with gzip.open(output_eventsfilename_eye1, "wb") as f:
f.write(content.encode())

e2b_log.info(f"file generated: {output_eventsfilename_eye1}")

output_eventsfilename_eye2 = generate_output_filename(
output_dir=output_dir,
input_file=input_file,
suffix="_recording-eye2_physioevents",
extension="tsv.gz",
)
content = physioevents_eye2.to_csv(
sep="\t", index=False, na_rep="n/a", header=None
)
with gzip.open(output_eventsfilename_eye2, "wb") as f:
f.write(content.encode())

e2b_log.info(f"file generated: {output_eventsfilename_eye2}")


def generate_output_filename(
Expand Down
Loading
Loading