Skip to content

Commit 95a51d1

Browse files
authored
Merge pull request #109 from ttngu207/main
Primarily fixing suite2p loader for missing ROI detection or trace extraction - pulling from staging for other minor updates
2 parents ef4b822 + 20cf21e commit 95a51d1

File tree

4 files changed

+157
-74
lines changed

4 files changed

+157
-74
lines changed

element_interface/dandi.py

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import os
22
import subprocess
33

4-
from dandi.download import download
54
from dandi.upload import upload
65

76

@@ -13,6 +12,8 @@ def upload_to_dandi(
1312
api_key: str = None,
1413
sync: bool = False,
1514
existing: str = "refresh",
15+
validation: str = "required",
16+
shell=True, # without this param, subprocess interprets first arg as file/dir
1617
):
1718
"""Upload NWB files to DANDI Archive
1819
@@ -27,6 +28,7 @@ def upload_to_dandi(
2728
sync (str, optional): If True, delete all files in archive that are not present
2829
in the local directory.
2930
existing (str, optional): see full description from `dandi upload --help`
31+
validation (str, optional): [require|skip|ignore] see full description from `dandi upload --help`
3032
"""
3133

3234
working_directory = working_directory or os.path.curdir
@@ -38,29 +40,46 @@ def upload_to_dandi(
3840
working_directory, str(dandiset_id)
3941
) # enforce str
4042

41-
dandiset_url = f"https://gui-staging.dandiarchive.org/#/dandiset/{dandiset_id}" if staging else f"https://dandiarchive.org/dandiset/{dandiset_id}/draft"
42-
43-
subprocess.run(
44-
["dandi", "download", "--download", "dandiset.yaml", "-o", working_directory, dandiset_url],
45-
shell=True,
43+
dandiset_url = (
44+
f"https://gui-staging.dandiarchive.org/#/dandiset/{dandiset_id}"
45+
if staging
46+
else f"https://dandiarchive.org/dandiset/{dandiset_id}/draft"
4647
)
4748

4849
subprocess.run(
49-
["dandi", "organize", "-d", dandiset_directory, data_directory, "-f", "dry"],
50-
shell=True, # without this param, subprocess interprets first arg as file/dir
50+
[
51+
"dandi",
52+
"download",
53+
"--download",
54+
"dandiset.yaml",
55+
"-o",
56+
working_directory,
57+
dandiset_url,
58+
],
59+
shell=shell,
5160
)
5261

5362
subprocess.run(
54-
["dandi", "organize", "-d", dandiset_directory, data_directory], shell=True
63+
[
64+
"dandi",
65+
"organize",
66+
"-d",
67+
dandiset_directory,
68+
data_directory,
69+
"--required-field",
70+
"subject_id",
71+
"--required-field",
72+
"session_id",
73+
],
74+
shell=shell,
5575
)
5676

57-
subprocess.run(
58-
["dandi", "validate", dandiset_directory], shell=True
59-
)
77+
subprocess.run(["dandi", "validate", dandiset_directory], shell=shell)
6078

6179
upload(
6280
paths=[dandiset_directory],
6381
dandi_instance="dandi-staging" if staging else "dandi",
6482
existing=existing,
6583
sync=sync,
84+
validation=validation,
6685
)
Lines changed: 120 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,114 @@
11
import pathlib
2+
from pathlib import Path
23
import xml.etree.ElementTree as ET
34
from datetime import datetime
4-
55
import numpy as np
66

77

8-
def get_prairieview_metadata(ome_tif_filepath: str) -> dict:
9-
"""Extract metadata for scans generated by Prairie View acquisition software.
8+
class PrairieViewMeta:
109

11-
The Prairie View software generates one `.ome.tif` imaging file per frame
12-
acquired. The metadata for all frames is contained in one .xml file. This
13-
function locates the .xml file and generates a dictionary necessary to
14-
populate the DataJoint `ScanInfo` and `Field` tables. Prairie View works
15-
with resonance scanners with a single field. Prairie View does not support
16-
bidirectional x and y scanning. ROI information is not contained in the
17-
`.xml` file. All images generated using Prairie View have square dimensions(e.g. 512x512).
10+
def __init__(self, prairieview_dir: str):
11+
"""Initialize PrairieViewMeta loader class
1812
19-
Args:
20-
ome_tif_filepath: An absolute path to the .ome.tif image file.
13+
Args:
14+
prairieview_dir (str): string, absolute file path to directory containing PrairieView dataset
15+
"""
16+
# ---- Search and verify CaImAn output file exists ----
17+
# May return multiple xml files. Only need one that contains scan metadata.
18+
self.prairieview_dir = Path(prairieview_dir)
2119

22-
Raises:
23-
FileNotFoundError: No .xml file containing information about the acquired scan
24-
was found at path in parent directory at `ome_tif_filepath`.
20+
for file in self.prairieview_dir.glob("*.xml"):
21+
xml_tree = ET.parse(file)
22+
xml_root = xml_tree.getroot()
23+
if xml_root.find(".//Sequence"):
24+
self.xml_file = file
25+
self._xml_root = xml_root
26+
break
27+
else:
28+
raise FileNotFoundError(
29+
f"No PrarieView metadata .xml file found at {prairieview_dir}"
30+
)
2531

26-
Returns:
27-
metainfo: A dict mapping keys to corresponding metadata values fetched from the
28-
.xml file.
29-
"""
32+
self._meta = None
3033

31-
# May return multiple xml files. Only need one that contains scan metadata.
32-
xml_files_list = pathlib.Path(ome_tif_filepath).parent.glob("*.xml")
34+
@property
35+
def meta(self):
36+
if self._meta is None:
37+
self._meta = _extract_prairieview_metadata(self.xml_file)
38+
return self._meta
3339

34-
for file in xml_files_list:
35-
xml_tree = ET.parse(file)
36-
xml_file = xml_tree.getroot()
37-
if xml_file.find(".//Sequence"):
38-
break
39-
else:
40-
raise FileNotFoundError(
41-
f"No PrarieView metadata .xml file found at {pathlib.Path(ome_tif_filepath).parent}"
42-
)
40+
def get_prairieview_files(self, plane_idx=None, channel=None):
41+
if plane_idx is None:
42+
if self.meta['num_planes'] > 1:
43+
raise ValueError(f"Please specify 'plane_idx' - Plane indices: {self.meta['plane_indices']}")
44+
else:
45+
plane_idx = self.meta['plane_indices'][0]
46+
else:
47+
assert plane_idx in self.meta['plane_indices'], f"Invalid 'plane_idx' - Plane indices: {self.meta['plane_indices']}"
48+
49+
if channel is None:
50+
if self.meta['num_channels'] > 1:
51+
raise ValueError(f"Please specify 'channel' - Channels: {self.meta['channels']}")
52+
else:
53+
plane_idx = self.meta['channels'][0]
54+
else:
55+
assert channel in self.meta['channels'], f"Invalid 'channel' - Channels: {self.meta['channels']}"
56+
57+
frames = self._xml_root.findall(f".//Sequence/Frame/[@index='{plane_idx}']/File/[@channel='{channel}']")
58+
return [f.attrib['filename'] for f in frames]
59+
60+
61+
def _extract_prairieview_metadata(xml_filepath: str):
62+
xml_filepath = Path(xml_filepath)
63+
if not xml_filepath.exists():
64+
raise FileNotFoundError(f"{xml_filepath} does not exist")
65+
xml_tree = ET.parse(xml_filepath)
66+
xml_root = xml_tree.getroot()
4367

4468
bidirectional_scan = False # Does not support bidirectional
4569
roi = 0
4670
n_fields = 1 # Always contains 1 field
47-
recording_start_time = xml_file.find(".//Sequence/[@cycle='1']").attrib.get("time")
71+
recording_start_time = xml_root.find(".//Sequence/[@cycle='1']").attrib.get("time")
4872

4973
# Get all channels and find unique values
5074
channel_list = [
5175
int(channel.attrib.get("channel"))
52-
for channel in xml_file.iterfind(".//Sequence/Frame/File/[@channel]")
76+
for channel in xml_root.iterfind(".//Sequence/Frame/File/[@channel]")
5377
]
54-
n_channels = len(set(channel_list))
55-
n_frames = len(xml_file.findall(".//Sequence/Frame"))
78+
channels = set(channel_list)
79+
n_channels = len(channels)
80+
n_frames = len(xml_root.findall(".//Sequence/Frame"))
5681
framerate = 1 / float(
57-
xml_file.findall('.//PVStateValue/[@key="framePeriod"]')[0].attrib.get("value")
82+
xml_root.findall('.//PVStateValue/[@key="framePeriod"]')[0].attrib.get("value")
5883
) # rate = 1/framePeriod
5984

6085
usec_per_line = (
6186
float(
62-
xml_file.findall(".//PVStateValue/[@key='scanLinePeriod']")[0].attrib.get(
87+
xml_root.findall(".//PVStateValue/[@key='scanLinePeriod']")[0].attrib.get(
6388
"value"
6489
)
6590
)
6691
* 1e6
6792
) # Convert from seconds to microseconds
6893

6994
scan_datetime = datetime.strptime(
70-
xml_file.attrib.get("date"), "%m/%d/%Y %I:%M:%S %p"
95+
xml_root.attrib.get("date"), "%m/%d/%Y %I:%M:%S %p"
7196
)
7297

7398
total_scan_duration = float(
74-
xml_file.findall(".//Sequence/Frame")[-1].attrib.get("relativeTime")
99+
xml_root.findall(".//Sequence/Frame")[-1].attrib.get("relativeTime")
75100
)
76101

77102
pixel_height = int(
78-
xml_file.findall(".//PVStateValue/[@key='pixelsPerLine']")[0].attrib.get(
103+
xml_root.findall(".//PVStateValue/[@key='pixelsPerLine']")[0].attrib.get(
79104
"value"
80105
)
81106
)
82107
# All PrairieView-acquired images have square dimensions (512 x 512; 1024 x 1024)
83108
pixel_width = pixel_height
84109

85110
um_per_pixel = float(
86-
xml_file.find(
111+
xml_root.find(
87112
".//PVStateValue/[@key='micronsPerPixel']/IndexedValue/[@index='XAxis']"
88113
).attrib.get("value")
89114
)
@@ -92,43 +117,45 @@ def get_prairieview_metadata(ome_tif_filepath: str) -> dict:
92117

93118
# x and y coordinate values for the center of the field
94119
x_field = float(
95-
xml_file.find(
120+
xml_root.find(
96121
".//PVStateValue/[@key='currentScanCenter']/IndexedValue/[@index='XAxis']"
97122
).attrib.get("value")
98123
)
99124
y_field = float(
100-
xml_file.find(
125+
xml_root.find(
101126
".//PVStateValue/[@key='currentScanCenter']/IndexedValue/[@index='YAxis']"
102127
).attrib.get("value")
103128
)
129+
104130
if (
105-
xml_file.find(
131+
xml_root.find(
106132
".//Sequence/[@cycle='1']/Frame/PVStateShard/PVStateValue/[@key='positionCurrent']/SubindexedValues/[@index='ZAxis']"
107133
)
108134
is None
109135
):
110136
z_fields = np.float64(
111-
xml_file.find(
137+
xml_root.find(
112138
".//PVStateValue/[@key='positionCurrent']/SubindexedValues/[@index='ZAxis']/SubindexedValue"
113139
).attrib.get("value")
114140
)
115141
n_depths = 1
142+
plane_indices = {0}
116143
assert z_fields.size == n_depths
117144
bidirection_z = False
118-
119145
else:
120146
bidirection_z = (
121-
xml_file.find(".//Sequence").attrib.get("bidirectionalZ") == "True"
147+
xml_root.find(".//Sequence").attrib.get("bidirectionalZ") == "True"
122148
)
123149

124150
# One "Frame" per depth in the .xml file. Gets number of frames in first sequence
125151
planes = [
126152
int(plane.attrib.get("index"))
127-
for plane in xml_file.findall(".//Sequence/[@cycle='1']/Frame")
153+
for plane in xml_root.findall(".//Sequence/[@cycle='1']/Frame")
128154
]
129-
n_depths = len(set(planes))
155+
plane_indices = set(planes)
156+
n_depths = len(plane_indices)
130157

131-
z_controllers = xml_file.findall(
158+
z_controllers = xml_root.findall(
132159
".//Sequence/[@cycle='1']/Frame/[@index='1']/PVStateShard/PVStateValue/[@key='positionCurrent']/SubindexedValues/[@index='ZAxis']/SubindexedValue"
133160
)
134161

@@ -137,13 +164,13 @@ def get_prairieview_metadata(ome_tif_filepath: str) -> dict:
137164
# must change depths.
138165
if len(z_controllers) > 1:
139166
z_repeats = []
140-
for controller in xml_file.findall(
167+
for controller in xml_root.findall(
141168
".//Sequence/[@cycle='1']/Frame/[@index='1']/PVStateShard/PVStateValue/[@key='positionCurrent']/SubindexedValues/[@index='ZAxis']/"
142169
):
143170
z_repeats.append(
144171
[
145172
float(z.attrib.get("value"))
146-
for z in xml_file.findall(
173+
for z in xml_root.findall(
147174
".//Sequence/[@cycle='1']/Frame/PVStateShard/PVStateValue/[@key='positionCurrent']/SubindexedValues/[@index='ZAxis']/SubindexedValue/[@subindex='{0}']".format(
148175
controller.attrib.get("subindex")
149176
)
@@ -163,7 +190,7 @@ def get_prairieview_metadata(ome_tif_filepath: str) -> dict:
163190
else:
164191
z_fields = [
165192
z.attrib.get("value")
166-
for z in xml_file.findall(
193+
for z in xml_root.findall(
167194
".//Sequence/[@cycle='1']/Frame/PVStateShard/PVStateValue/[@key='positionCurrent']/SubindexedValues/[@index='ZAxis']/SubindexedValue/[@subindex='0']"
168195
)
169196
]
@@ -195,6 +222,47 @@ def get_prairieview_metadata(ome_tif_filepath: str) -> dict:
195222
fieldY=y_field,
196223
fieldZ=z_fields,
197224
recording_time=recording_start_time,
225+
channels=list(channels),
226+
plane_indices=list(plane_indices),
198227
)
199228

200229
return metainfo
230+
231+
232+
def get_prairieview_metadata(ome_tif_filepath: str) -> dict:
233+
"""Extract metadata for scans generated by Prairie View acquisition software.
234+
235+
The Prairie View software generates one `.ome.tif` imaging file per frame
236+
acquired. The metadata for all frames is contained in one .xml file. This
237+
function locates the .xml file and generates a dictionary necessary to
238+
populate the DataJoint `ScanInfo` and `Field` tables. Prairie View works
239+
with resonance scanners with a single field. Prairie View does not support
240+
bidirectional x and y scanning. ROI information is not contained in the
241+
`.xml` file. All images generated using Prairie View have square dimensions(e.g. 512x512).
242+
243+
Args:
244+
ome_tif_filepath: An absolute path to the .ome.tif image file.
245+
246+
Raises:
247+
FileNotFoundError: No .xml file containing information about the acquired scan
248+
was found at path in parent directory at `ome_tif_filepath`.
249+
250+
Returns:
251+
metainfo: A dict mapping keys to corresponding metadata values fetched from the
252+
.xml file.
253+
"""
254+
255+
# May return multiple xml files. Only need one that contains scan metadata.
256+
xml_files_list = pathlib.Path(ome_tif_filepath).parent.glob("*.xml")
257+
258+
for file in xml_files_list:
259+
xml_tree = ET.parse(file)
260+
xml_file = xml_tree.getroot()
261+
if xml_file.find(".//Sequence"):
262+
break
263+
else:
264+
raise FileNotFoundError(
265+
f"No PrarieView metadata .xml file found at {pathlib.Path(ome_tif_filepath).parent}"
266+
)
267+
268+
return _extract_prairieview_metadata(file)

element_interface/suite2p_loader.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -138,15 +138,6 @@ def __init__(self, suite2p_plane_dir: str):
138138
)
139139
self.creation_time = datetime.fromtimestamp(ops_fp.stat().st_ctime)
140140

141-
iscell_fp = self.fpath / "iscell.npy"
142-
if not iscell_fp.exists():
143-
raise FileNotFoundError(
144-
'No "iscell.npy" found. Invalid suite2p plane folder: {}'.format(
145-
self.fpath
146-
)
147-
)
148-
self.curation_time = datetime.fromtimestamp(iscell_fp.stat().st_ctime)
149-
150141
# -- Initialize attributes --
151142
for s2p_type in _suite2p_ftypes:
152143
setattr(self, "_{}".format(s2p_type), None)
@@ -160,6 +151,11 @@ def __init__(self, suite2p_plane_dir: str):
160151

161152
# -- load core files --
162153

154+
@property
155+
def curation_time(self):
156+
print("DeprecationWarning: 'curation_time' is deprecated, set to be the same as 'creation time', no longer reliable.")
157+
return self.creation_time
158+
163159
@property
164160
def ops(self):
165161
if self._ops is None:

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
dandi
1+
dandi>=0.56.0
22
numpy

0 commit comments

Comments
 (0)