clabtoolkit 0.3.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- clabtoolkit/__init__.py +5 -0
- clabtoolkit/bidstools.py +2260 -0
- clabtoolkit/config/bids.json +147 -0
- clabtoolkit/config/config.json +58 -0
- clabtoolkit/config/lobes.json +112 -0
- clabtoolkit/config/stats_mapping.json +140 -0
- clabtoolkit/config/viz_views.json +268 -0
- clabtoolkit/connectivitytools.py +0 -0
- clabtoolkit/dicomtools.py +941 -0
- clabtoolkit/dwitools.py +1999 -0
- clabtoolkit/freesurfertools.py +5378 -0
- clabtoolkit/imagetools.py +2734 -0
- clabtoolkit/misctools.py +5204 -0
- clabtoolkit/morphometrytools.py +2970 -0
- clabtoolkit/networktools.py +351 -0
- clabtoolkit/parcellationtools.py +3239 -0
- clabtoolkit/pipelinetools.py +1187 -0
- clabtoolkit/plottools.py +682 -0
- clabtoolkit/qcqatools.py +1305 -0
- clabtoolkit/segmentationtools.py +356 -0
- clabtoolkit/surfacetools.py +2785 -0
- clabtoolkit/visualizationtools.py +5575 -0
- clabtoolkit-0.3.3.dist-info/METADATA +212 -0
- clabtoolkit-0.3.3.dist-info/RECORD +28 -0
- clabtoolkit-0.3.3.dist-info/WHEEL +5 -0
- clabtoolkit-0.3.3.dist-info/licenses/AUTHORS.rst +13 -0
- clabtoolkit-0.3.3.dist-info/licenses/LICENSE +16 -0
- clabtoolkit-0.3.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1187 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import time
|
|
5
|
+
import queue
|
|
6
|
+
import threading
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from typing import Union, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
import json
|
|
13
|
+
from glob import glob
|
|
14
|
+
|
|
15
|
+
from rich.progress import (
|
|
16
|
+
Progress,
|
|
17
|
+
BarColumn,
|
|
18
|
+
TimeRemainingColumn,
|
|
19
|
+
TextColumn,
|
|
20
|
+
MofNCompleteColumn,
|
|
21
|
+
SpinnerColumn,
|
|
22
|
+
)
|
|
23
|
+
from rich.console import Console
|
|
24
|
+
from rich.panel import Panel
|
|
25
|
+
|
|
26
|
+
# Importing the clabtoolkit modules
|
|
27
|
+
from . import misctools as cltmisc
|
|
28
|
+
from . import bidstools as cltbids
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
####################################################################################################
|
|
32
|
+
####################################################################################################
|
|
33
|
+
############ ############
|
|
34
|
+
############ ############
|
|
35
|
+
############ Section 1: Utility methods ############
|
|
36
|
+
############ ############
|
|
37
|
+
############ ############
|
|
38
|
+
####################################################################################################
|
|
39
|
+
####################################################################################################
|
|
40
|
+
def get_ids2process(
|
|
41
|
+
ids: Union[str, List[str], None] = None, in_dir: str = None
|
|
42
|
+
) -> List[str]:
|
|
43
|
+
"""
|
|
44
|
+
Get list of subject IDs to process from various input sources.
|
|
45
|
+
|
|
46
|
+
Parameters
|
|
47
|
+
----------
|
|
48
|
+
ids : str, list of str, or None, optional
|
|
49
|
+
Subject IDs specification. Can be:
|
|
50
|
+
- None: discover all subjects in `in_dir` (default)
|
|
51
|
+
- list: list of subject ID strings
|
|
52
|
+
- str: comma-separated IDs, single ID, or path to text file
|
|
53
|
+
|
|
54
|
+
in_dir : str, optional
|
|
55
|
+
Directory path to scan for subjects when `ids` is None.
|
|
56
|
+
Only used when `ids` is None.
|
|
57
|
+
|
|
58
|
+
Returns
|
|
59
|
+
-------
|
|
60
|
+
list of str
|
|
61
|
+
List of subject ID strings, with empty entries filtered out.
|
|
62
|
+
|
|
63
|
+
Raises
|
|
64
|
+
------
|
|
65
|
+
ValueError
|
|
66
|
+
If `ids` is not None/list/str, or if `in_dir` is invalid when `ids` is None.
|
|
67
|
+
FileNotFoundError
|
|
68
|
+
If specified file path in `ids` does not exist.
|
|
69
|
+
IOError
|
|
70
|
+
If file cannot be read due to permissions or other IO issues.
|
|
71
|
+
|
|
72
|
+
Examples
|
|
73
|
+
--------
|
|
74
|
+
>>> # Discover subjects from directory
|
|
75
|
+
>>> get_ids2process(ids=None, in_dir='/data/subjects')
|
|
76
|
+
['sub-001', 'sub-002', 'sub-003']
|
|
77
|
+
|
|
78
|
+
>>> # From list
|
|
79
|
+
>>> get_ids2process(['sub-001', 'sub-002'])
|
|
80
|
+
['sub-001', 'sub-002']
|
|
81
|
+
|
|
82
|
+
>>> # From comma-separated string
|
|
83
|
+
>>> get_ids2process('sub-001, sub-002, sub-003')
|
|
84
|
+
['sub-001', 'sub-002', 'sub-003']
|
|
85
|
+
|
|
86
|
+
>>> # Single subject ID
|
|
87
|
+
>>> get_ids2process('sub-001')
|
|
88
|
+
['sub-001']
|
|
89
|
+
|
|
90
|
+
>>> # From text file
|
|
91
|
+
>>> get_ids2process('/path/to/subjects.txt')
|
|
92
|
+
['sub-001', 'sub-002', 'sub-003']
|
|
93
|
+
|
|
94
|
+
Notes
|
|
95
|
+
-----
|
|
96
|
+
When scanning directories (ids=None), only directories starting with 'sub-'
|
|
97
|
+
are considered valid subject directories.
|
|
98
|
+
|
|
99
|
+
Text files should contain one subject ID per line. Empty lines and
|
|
100
|
+
whitespace are automatically filtered out.
|
|
101
|
+
"""
|
|
102
|
+
# Handle None case - discover from directory
|
|
103
|
+
if ids is None:
|
|
104
|
+
if not in_dir or not os.path.isdir(in_dir):
|
|
105
|
+
raise ValueError(f"Valid in_dir required when ids is None. Got: {in_dir}")
|
|
106
|
+
return [d for d in os.listdir(in_dir) if d.startswith("sub-")]
|
|
107
|
+
|
|
108
|
+
# Handle list case
|
|
109
|
+
if isinstance(ids, list):
|
|
110
|
+
return [str(id_).strip() for id_ in ids if str(id_).strip()]
|
|
111
|
+
|
|
112
|
+
# Handle string case
|
|
113
|
+
if isinstance(ids, str):
|
|
114
|
+
ids = ids.strip()
|
|
115
|
+
if not ids:
|
|
116
|
+
return []
|
|
117
|
+
|
|
118
|
+
# File path
|
|
119
|
+
if os.path.isfile(ids):
|
|
120
|
+
with open(ids, "r", encoding="utf-8") as f:
|
|
121
|
+
return [line.strip() for line in f if line.strip()]
|
|
122
|
+
|
|
123
|
+
# Comma-separated or single ID
|
|
124
|
+
return [id_.strip() for id_ in ids.split(",") if id_.strip()]
|
|
125
|
+
|
|
126
|
+
raise ValueError(f"ids must be None, list, or string, got {type(ids)}")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
####################################################################################################
|
|
130
|
+
####################################################################################################
|
|
131
|
+
############ ############
|
|
132
|
+
############ ############
|
|
133
|
+
############ Section 2: Methods for assessing the processing status of the pipelines ############
|
|
134
|
+
############ ############
|
|
135
|
+
############ ############
|
|
136
|
+
####################################################################################################
|
|
137
|
+
####################################################################################################
|
|
138
|
+
def create_processing_status_table(
|
|
139
|
+
deriv_dir: str,
|
|
140
|
+
subj_ids: Union[list, str],
|
|
141
|
+
output_table: str = None,
|
|
142
|
+
n_jobs: int = -1,
|
|
143
|
+
):
|
|
144
|
+
"""
|
|
145
|
+
This method creates a table with the processing status of the subjects in the BIDs derivatives directory.
|
|
146
|
+
Uses parallel processing for improved performance with rich progress visualization.
|
|
147
|
+
|
|
148
|
+
Parameters
|
|
149
|
+
----------
|
|
150
|
+
deriv_dir : str
|
|
151
|
+
Path to the derivatives directory.
|
|
152
|
+
|
|
153
|
+
subj_ids : list or str
|
|
154
|
+
List of subject IDs or a text file containing the subject IDs.
|
|
155
|
+
|
|
156
|
+
output_table : str, optional
|
|
157
|
+
Path to save the resulting table. If None, the table is not saved.
|
|
158
|
+
|
|
159
|
+
n_jobs : int, optional
|
|
160
|
+
Number of parallel jobs to run. Default is -1 which uses all available cores.
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
Returns
|
|
164
|
+
-------
|
|
165
|
+
pd.DataFrame
|
|
166
|
+
DataFrame containing the processing status of the subjects.
|
|
167
|
+
|
|
168
|
+
str
|
|
169
|
+
Path to the saved table if output_table is provided, otherwise None.
|
|
170
|
+
|
|
171
|
+
Raises
|
|
172
|
+
------
|
|
173
|
+
FileNotFoundError
|
|
174
|
+
If the derivatives directory or the subject IDs file does not exist.
|
|
175
|
+
ValueError
|
|
176
|
+
If no derivatives folders are found or if the subject IDs list is empty.
|
|
177
|
+
|
|
178
|
+
TypeError
|
|
179
|
+
If subj_ids is not a list or a string path to a file.
|
|
180
|
+
|
|
181
|
+
Examples
|
|
182
|
+
--------
|
|
183
|
+
>>> deriv_dir = "/path/to/derivatives"
|
|
184
|
+
>>> subj_ids = ["sub-01", "sub-02"]
|
|
185
|
+
>>> output_table = "/path/to/output_table.csv"
|
|
186
|
+
>>> df, saved_path = create_processing_status_table(deriv_dir, subj_ids, output_table)
|
|
187
|
+
>>> print(df)
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
from joblib import Parallel, delayed
|
|
191
|
+
from . import morphometrytools as cltmorpho
|
|
192
|
+
|
|
193
|
+
# Initialize rich console
|
|
194
|
+
console = Console()
|
|
195
|
+
|
|
196
|
+
# Check if the derivatives directory exists
|
|
197
|
+
deriv_dir = cltmisc.remove_trailing_separators(deriv_dir)
|
|
198
|
+
|
|
199
|
+
if not os.path.isdir(deriv_dir):
|
|
200
|
+
raise FileNotFoundError(
|
|
201
|
+
f"The derivatives directory {deriv_dir} does not exist."
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Process subject IDs
|
|
205
|
+
if isinstance(subj_ids, str):
|
|
206
|
+
if not os.path.isfile(subj_ids):
|
|
207
|
+
raise FileNotFoundError(f"The file {subj_ids} does not exist.")
|
|
208
|
+
else:
|
|
209
|
+
with open(subj_ids, "r") as f:
|
|
210
|
+
subj_list = f.read().splitlines()
|
|
211
|
+
elif isinstance(subj_ids, list):
|
|
212
|
+
if len(subj_ids) == 0:
|
|
213
|
+
raise ValueError("The list of subject IDs is empty.")
|
|
214
|
+
else:
|
|
215
|
+
subj_list = subj_ids
|
|
216
|
+
else:
|
|
217
|
+
raise TypeError("subj_ids must be a list or a string path to a file")
|
|
218
|
+
|
|
219
|
+
# Number of Ids
|
|
220
|
+
n_subj = len(subj_list)
|
|
221
|
+
|
|
222
|
+
# Find all the derivatives folders
|
|
223
|
+
pipe_dirs = cltbids.get_derivatives_folders(deriv_dir)
|
|
224
|
+
|
|
225
|
+
if len(pipe_dirs) == 0:
|
|
226
|
+
raise ValueError(
|
|
227
|
+
"No derivatives folders were found in the specified directory."
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# Create a message queue to communicate across threads
|
|
231
|
+
progress_queue = queue.Queue()
|
|
232
|
+
|
|
233
|
+
# Function to process a single subject
|
|
234
|
+
def process_subject(full_id):
|
|
235
|
+
try:
|
|
236
|
+
# Parse the subject ID
|
|
237
|
+
id_dict = cltbids.str2entity(full_id)
|
|
238
|
+
subject = id_dict["sub"]
|
|
239
|
+
|
|
240
|
+
# Get entity information for this subject
|
|
241
|
+
ent_list = cltbids.entities4table(selected_entities=full_id)
|
|
242
|
+
df_add = cltbids.entities_to_table(
|
|
243
|
+
filepath=full_id, entities_to_extract=ent_list
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# Create a new DataFrame for this subject's processing status
|
|
247
|
+
proc_table = pd.DataFrame(
|
|
248
|
+
columns=pipe_dirs, index=[0]
|
|
249
|
+
) # Single row for this subject
|
|
250
|
+
|
|
251
|
+
# Remove suffix and extension from entities
|
|
252
|
+
clean_id_dict = id_dict.copy()
|
|
253
|
+
if "suffix" in clean_id_dict:
|
|
254
|
+
del clean_id_dict["suffix"]
|
|
255
|
+
if "extension" in clean_id_dict:
|
|
256
|
+
del clean_id_dict["extension"]
|
|
257
|
+
|
|
258
|
+
# Create list of entity key-value pairs
|
|
259
|
+
subj_ent = [f"{k}-{v}" for k, v in clean_id_dict.items()]
|
|
260
|
+
|
|
261
|
+
# Process each derivatives directory
|
|
262
|
+
for tmp_pipe_deriv in pipe_dirs:
|
|
263
|
+
# Find subject's directory in this pipeline
|
|
264
|
+
ind_der_dir = glob(
|
|
265
|
+
os.path.join(
|
|
266
|
+
deriv_dir, tmp_pipe_deriv, "sub-" + clean_id_dict["sub"] + "*"
|
|
267
|
+
)
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# Filter if multiple directories found
|
|
271
|
+
if len(ind_der_dir) > 1:
|
|
272
|
+
ind_der_dir = cltmisc.filter_by_substring(
|
|
273
|
+
ind_der_dir,
|
|
274
|
+
or_filter=[clean_id_dict["sub"]],
|
|
275
|
+
and_filter=subj_ent,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# Set count to 0 if no directory found
|
|
279
|
+
if len(ind_der_dir) == 0:
|
|
280
|
+
proc_table.at[0, tmp_pipe_deriv] = 0
|
|
281
|
+
continue
|
|
282
|
+
|
|
283
|
+
# Count files for this subject in this pipeline
|
|
284
|
+
all_pip_files = cltmisc.get_all_files(ind_der_dir[0])
|
|
285
|
+
subj_pipe_files = cltmisc.filter_by_substring(
|
|
286
|
+
all_pip_files, or_filter=clean_id_dict["sub"], and_filter=subj_ent
|
|
287
|
+
)
|
|
288
|
+
n_files = len(subj_pipe_files)
|
|
289
|
+
|
|
290
|
+
# Store the count
|
|
291
|
+
proc_table.at[0, tmp_pipe_deriv] = n_files
|
|
292
|
+
|
|
293
|
+
# Combine the entity info with processing counts
|
|
294
|
+
subj_proc_table = cltmisc.expand_and_concatenate(df_add, proc_table)
|
|
295
|
+
|
|
296
|
+
# Signal completion through the queue
|
|
297
|
+
progress_queue.put((True, full_id))
|
|
298
|
+
|
|
299
|
+
return subj_proc_table
|
|
300
|
+
except Exception as e:
|
|
301
|
+
# Signal error through the queue
|
|
302
|
+
progress_queue.put((False, f"{full_id}: {str(e)}"))
|
|
303
|
+
raise e
|
|
304
|
+
|
|
305
|
+
# Use Rich for progress tracking
|
|
306
|
+
all_results = []
|
|
307
|
+
stop_monitor = threading.Event()
|
|
308
|
+
|
|
309
|
+
# Start a separate thread for the progress bar
|
|
310
|
+
def progress_monitor(progress_queue, total_subjects, progress_task, stop_event):
|
|
311
|
+
completed = 0
|
|
312
|
+
errors = 0
|
|
313
|
+
|
|
314
|
+
while (completed + errors < total_subjects) and not stop_event.is_set():
|
|
315
|
+
try:
|
|
316
|
+
success, message = progress_queue.get(timeout=0.1)
|
|
317
|
+
|
|
318
|
+
if success:
|
|
319
|
+
completed += 1
|
|
320
|
+
else:
|
|
321
|
+
errors += 1
|
|
322
|
+
console.print(f"[bold red]Error: {message}[/bold red]")
|
|
323
|
+
|
|
324
|
+
# Update progress bar
|
|
325
|
+
progress.update(
|
|
326
|
+
progress_task,
|
|
327
|
+
completed=completed,
|
|
328
|
+
description=f"[yellow]Processing subjects - {completed}/{total_subjects} completed",
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
# Important: mark the task as done in the queue
|
|
332
|
+
progress_queue.task_done()
|
|
333
|
+
|
|
334
|
+
except queue.Empty:
|
|
335
|
+
# No updates, just continue waiting
|
|
336
|
+
pass
|
|
337
|
+
|
|
338
|
+
# Ensure the progress bar shows 100% completion
|
|
339
|
+
if not stop_event.is_set():
|
|
340
|
+
progress.update(progress_task, completed=total_subjects)
|
|
341
|
+
|
|
342
|
+
with Progress(
|
|
343
|
+
SpinnerColumn(),
|
|
344
|
+
TextColumn("[bold blue]{task.description}"),
|
|
345
|
+
BarColumn(bar_width=50),
|
|
346
|
+
TextColumn("[progress.percentage]{task.percentage:>3.1f}%"),
|
|
347
|
+
TimeRemainingColumn(),
|
|
348
|
+
MofNCompleteColumn(),
|
|
349
|
+
console=console,
|
|
350
|
+
refresh_per_second=10, # Increase refresh rate
|
|
351
|
+
) as progress:
|
|
352
|
+
# Add main task to track progress
|
|
353
|
+
main_task = progress.add_task("[yellow]Processing subjects", total=n_subj)
|
|
354
|
+
|
|
355
|
+
# Start progress monitor thread
|
|
356
|
+
monitor_thread = threading.Thread(
|
|
357
|
+
target=progress_monitor,
|
|
358
|
+
args=(progress_queue, n_subj, main_task, stop_monitor),
|
|
359
|
+
daemon=True,
|
|
360
|
+
)
|
|
361
|
+
monitor_thread.start()
|
|
362
|
+
|
|
363
|
+
try:
|
|
364
|
+
# Process subjects in parallel with joblib
|
|
365
|
+
results = Parallel(n_jobs=n_jobs, backend="threading", verbose=0)(
|
|
366
|
+
delayed(process_subject)(subject) for subject in subj_list
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
# Allow some time for the queue to process final updates
|
|
370
|
+
time.sleep(0.5)
|
|
371
|
+
|
|
372
|
+
# Directly set progress to 100% after all processing is done
|
|
373
|
+
progress.update(main_task, completed=n_subj, refresh=True)
|
|
374
|
+
|
|
375
|
+
# Wait for the progress queue to be empty
|
|
376
|
+
progress_queue.join()
|
|
377
|
+
|
|
378
|
+
finally:
|
|
379
|
+
# Signal the monitor thread to stop
|
|
380
|
+
stop_monitor.set()
|
|
381
|
+
|
|
382
|
+
# Make absolutely sure progress shows completion
|
|
383
|
+
progress.update(main_task, completed=n_subj, refresh=True)
|
|
384
|
+
|
|
385
|
+
# Combine all results
|
|
386
|
+
proc_status_df = pd.concat(results, ignore_index=True)
|
|
387
|
+
|
|
388
|
+
# Save table if requested
|
|
389
|
+
if output_table is not None:
|
|
390
|
+
output_dir = os.path.dirname(output_table)
|
|
391
|
+
if output_dir and not os.path.exists(output_dir):
|
|
392
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
393
|
+
proc_status_df.to_csv(output_table, sep="\t", index=False)
|
|
394
|
+
|
|
395
|
+
return proc_status_df, output_table
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
####################################################################################################
|
|
399
|
+
def get_processing_status_details_json(
|
|
400
|
+
proc_status_df: Union[str, dict],
|
|
401
|
+
subj_ids: Union[List[str], str],
|
|
402
|
+
deriv_dir: str,
|
|
403
|
+
pipe_dirs: Union[List[str], str] = None,
|
|
404
|
+
out_json: str = None,
|
|
405
|
+
only_ids: bool = False,
|
|
406
|
+
):
|
|
407
|
+
"""
|
|
408
|
+
This function creates a dictionary with the details of the processing status of the subjects in the BIDs derivatives directory.
|
|
409
|
+
It provides the IDs of the subjects with incomplete or mismatched number of files.
|
|
410
|
+
|
|
411
|
+
Parameters
|
|
412
|
+
----------
|
|
413
|
+
proc_status_df : str or dict
|
|
414
|
+
Path to the processing status DataFrame or a DataFrame itself. This DataFrame can be
|
|
415
|
+
obtained with the function "create_processing_status_table".
|
|
416
|
+
|
|
417
|
+
subj_ids : list or str
|
|
418
|
+
List of subject IDs or a text file containing the subject IDs.
|
|
419
|
+
|
|
420
|
+
deriv_dir : str
|
|
421
|
+
Path to the derivatives directory.
|
|
422
|
+
|
|
423
|
+
pipe_dirs : list or str, optional
|
|
424
|
+
List of processing pipelines to check. If None, all pipelines will be checked.
|
|
425
|
+
|
|
426
|
+
out_json : str, optional
|
|
427
|
+
Path to save the output JSON file. If None, the JSON file will not be saved.
|
|
428
|
+
|
|
429
|
+
only_ids : bool, optional
|
|
430
|
+
If True, only the IDs of the subjects with mismatches will be returned, without the file details.
|
|
431
|
+
|
|
432
|
+
Returns
|
|
433
|
+
-------
|
|
434
|
+
dict
|
|
435
|
+
Dictionary containing the details of the processing status of the subjects.
|
|
436
|
+
str
|
|
437
|
+
Path to the saved JSON file if out_json is provided, otherwise None.
|
|
438
|
+
"""
|
|
439
|
+
|
|
440
|
+
from . import morphometrytools as cltmorpho
|
|
441
|
+
import os
|
|
442
|
+
import numpy as np
|
|
443
|
+
|
|
444
|
+
if isinstance(proc_status_df, str):
|
|
445
|
+
if not os.path.isfile(proc_status_df):
|
|
446
|
+
raise FileNotFoundError(f"The file {proc_status_df} does not exist.")
|
|
447
|
+
else:
|
|
448
|
+
proc_status_df = cltmisc.smart_read_table(proc_status_df)
|
|
449
|
+
elif not isinstance(proc_status_df, pd.DataFrame):
|
|
450
|
+
raise TypeError("proc_status_df must be a DataFrame or a string path to a file")
|
|
451
|
+
|
|
452
|
+
# Process subject IDs
|
|
453
|
+
if isinstance(subj_ids, str):
|
|
454
|
+
if not os.path.isfile(subj_ids):
|
|
455
|
+
raise FileNotFoundError(f"The file {subj_ids} does not exist.")
|
|
456
|
+
else:
|
|
457
|
+
with open(subj_ids, "r") as f:
|
|
458
|
+
subj_list = f.read().splitlines()
|
|
459
|
+
elif isinstance(subj_ids, list):
|
|
460
|
+
if len(subj_ids) == 0:
|
|
461
|
+
raise ValueError("The list of subject IDs is empty.")
|
|
462
|
+
else:
|
|
463
|
+
subj_list = subj_ids
|
|
464
|
+
else:
|
|
465
|
+
raise TypeError("subj_ids must be a list or a string path to a file")
|
|
466
|
+
|
|
467
|
+
# Check if the derivatives directory exists
|
|
468
|
+
deriv_dir = cltmisc.remove_trailing_separators(deriv_dir)
|
|
469
|
+
|
|
470
|
+
if not os.path.isdir(deriv_dir):
|
|
471
|
+
raise FileNotFoundError(
|
|
472
|
+
f"The derivatives directory {deriv_dir} does not exist."
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
# Find all the derivatives folders
|
|
476
|
+
all_pipe_dirs = cltbids.get_derivatives_folders(deriv_dir)
|
|
477
|
+
|
|
478
|
+
if len(all_pipe_dirs) == 0:
|
|
479
|
+
raise ValueError(
|
|
480
|
+
"No derivatives folders were found in the specified directory."
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
if pipe_dirs is not None:
|
|
484
|
+
if isinstance(pipe_dirs, str):
|
|
485
|
+
pipe_dirs = [pipe_dirs]
|
|
486
|
+
|
|
487
|
+
pipe_dirs = cltmisc.filter_by_substring(all_pipe_dirs, or_filter=pipe_dirs)
|
|
488
|
+
else:
|
|
489
|
+
pipe_dirs = all_pipe_dirs
|
|
490
|
+
|
|
491
|
+
# All entities
|
|
492
|
+
ent_list = cltbids.entities4table()
|
|
493
|
+
|
|
494
|
+
# Get all the columns names
|
|
495
|
+
col_names = proc_status_df.columns.tolist()
|
|
496
|
+
|
|
497
|
+
# Get all the columns that are not in the pipe_dirs
|
|
498
|
+
subj_columns = list(set(col_names) - set(pipe_dirs))
|
|
499
|
+
|
|
500
|
+
subj_ids_df = proc_status_df[subj_columns]
|
|
501
|
+
|
|
502
|
+
# Create a consistent structure for the output dictionary
|
|
503
|
+
missmatch_summary = {}
|
|
504
|
+
|
|
505
|
+
# Process each pipeline
|
|
506
|
+
for i in pipe_dirs:
|
|
507
|
+
proc_status_df[i] = proc_status_df[i].astype(int)
|
|
508
|
+
pipe_dir_fold = os.path.join(deriv_dir, i)
|
|
509
|
+
|
|
510
|
+
# Initialize consistent structure
|
|
511
|
+
missmatch_pipe = {"ref_fullid": "", "missmatch_fullid": {}}
|
|
512
|
+
|
|
513
|
+
# Get the mode for the column to determine the reference value
|
|
514
|
+
mode_value = proc_status_df[i].mode()[0]
|
|
515
|
+
|
|
516
|
+
# Find rows that match the mode (will be used as reference)
|
|
517
|
+
agreement_rows = proc_status_df[proc_status_df[i] == mode_value].index
|
|
518
|
+
|
|
519
|
+
# Get reference subject details (using the first row that matches the mode)
|
|
520
|
+
ref_ids = subj_ids_df.loc[agreement_rows].iloc[0, :]
|
|
521
|
+
|
|
522
|
+
# Create identifiers for the reference subject
|
|
523
|
+
cad2look_ref = [
|
|
524
|
+
f"{key}-{ref_ids[value]}"
|
|
525
|
+
for key, value in ent_list.items()
|
|
526
|
+
if value in subj_columns
|
|
527
|
+
]
|
|
528
|
+
|
|
529
|
+
# Get files for the reference subject
|
|
530
|
+
ref_files = cltbids.get_individual_files_and_folders(
|
|
531
|
+
pipe_dir_fold,
|
|
532
|
+
cad2look_ref,
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
# Find the full ID of the reference subject
|
|
536
|
+
try:
|
|
537
|
+
ref_full_id = cltmisc.filter_by_substring(
|
|
538
|
+
subj_list, or_filter=cad2look_ref[0], and_filter=cad2look_ref
|
|
539
|
+
)[0]
|
|
540
|
+
except IndexError:
|
|
541
|
+
# Handle case where reference ID is not found
|
|
542
|
+
ref_full_id = "unknown_reference"
|
|
543
|
+
|
|
544
|
+
missmatch_pipe["ref_fullid"] = ref_full_id
|
|
545
|
+
|
|
546
|
+
# Find rows that don't match the mode (disagreement rows)
|
|
547
|
+
disagreement_rows = proc_status_df[proc_status_df[i] != mode_value].index
|
|
548
|
+
|
|
549
|
+
# Only process mismatches if reference files exist and there are disagreements
|
|
550
|
+
if ref_files and len(disagreement_rows) > 0:
|
|
551
|
+
# Process reference files to remove path prefixes for comparison
|
|
552
|
+
cad2look_ref.append(pipe_dir_fold)
|
|
553
|
+
tmp_ref_files = cltmisc.remove_substrings(ref_files, cad2look_ref)
|
|
554
|
+
|
|
555
|
+
# Get the ids of the subjects with disagreement
|
|
556
|
+
subtable_ids = subj_ids_df.loc[disagreement_rows]
|
|
557
|
+
|
|
558
|
+
# Loop through all subjects with disagreement
|
|
559
|
+
for j in range(len(disagreement_rows)):
|
|
560
|
+
# Get the subject ID
|
|
561
|
+
sub_row = subtable_ids.iloc[j, :]
|
|
562
|
+
|
|
563
|
+
# Create identifiers for this subject
|
|
564
|
+
cad2look_ind = [
|
|
565
|
+
f"{key}-{sub_row[value]}"
|
|
566
|
+
for key, value in ent_list.items()
|
|
567
|
+
if value in subj_columns
|
|
568
|
+
]
|
|
569
|
+
|
|
570
|
+
# Get files for this subject
|
|
571
|
+
indiv_files = cltbids.get_individual_files_and_folders(
|
|
572
|
+
pipe_dir_fold,
|
|
573
|
+
cad2look_ind,
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
try:
|
|
577
|
+
# Find the full ID of this subject
|
|
578
|
+
indiv_full_id = cltmisc.filter_by_substring(
|
|
579
|
+
subj_list,
|
|
580
|
+
or_filter=cad2look_ind[0],
|
|
581
|
+
and_filter=cad2look_ind,
|
|
582
|
+
)[0]
|
|
583
|
+
except IndexError:
|
|
584
|
+
# Handle case where subject ID is not found
|
|
585
|
+
indiv_full_id = f"unknown_subject_{j}"
|
|
586
|
+
|
|
587
|
+
# Initialize results for this subject
|
|
588
|
+
missmatch_subject = {"missing_files": [], "extra_files": []}
|
|
589
|
+
|
|
590
|
+
if indiv_files:
|
|
591
|
+
# Process individual files to remove path prefixes for comparison
|
|
592
|
+
cad2look_ind.append(pipe_dir_fold)
|
|
593
|
+
tmp_indiv_files = cltmisc.remove_substrings(
|
|
594
|
+
indiv_files, cad2look_ind
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
# Find missing files (in reference but not in this subject)
|
|
598
|
+
tmp_miss = list(set(tmp_ref_files) - set(tmp_indiv_files))
|
|
599
|
+
if tmp_miss:
|
|
600
|
+
miss_indices = cltmisc.get_indexes_by_substring(
|
|
601
|
+
tmp_ref_files, tmp_miss
|
|
602
|
+
)
|
|
603
|
+
selected_files_ref = [ref_files[i] for i in miss_indices]
|
|
604
|
+
missmatch_subject["missing_files"] = cltmisc.replace_substrings(
|
|
605
|
+
selected_files_ref, cad2look_ref, cad2look_ind
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
# Find extra files (in this subject but not in reference)
|
|
609
|
+
tmp_extra = list(set(tmp_indiv_files) - set(tmp_ref_files))
|
|
610
|
+
if tmp_extra:
|
|
611
|
+
extra_indices = cltmisc.get_indexes_by_substring(
|
|
612
|
+
tmp_indiv_files, tmp_extra
|
|
613
|
+
)
|
|
614
|
+
selected_files_indiv = [indiv_files[i] for i in extra_indices]
|
|
615
|
+
missmatch_subject["extra_files"] = cltmisc.replace_substrings(
|
|
616
|
+
selected_files_indiv, cad2look_ind, cad2look_ref
|
|
617
|
+
)
|
|
618
|
+
else:
|
|
619
|
+
# If no files found for this subject, all reference files are missing
|
|
620
|
+
missmatch_subject["missing_files"] = cltmisc.replace_substrings(
|
|
621
|
+
ref_files, cad2look_ref, cad2look_ind
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
# Add this subject's details to the results
|
|
625
|
+
missmatch_pipe["missmatch_fullid"][indiv_full_id] = missmatch_subject
|
|
626
|
+
|
|
627
|
+
# Add this pipeline's results to the summary
|
|
628
|
+
missmatch_summary[i] = missmatch_pipe
|
|
629
|
+
|
|
630
|
+
# If only_ids is True, simplify the output to just include IDs
|
|
631
|
+
if only_ids:
|
|
632
|
+
for i in missmatch_summary.keys():
|
|
633
|
+
missmatch_summary[i]["missmatch_fullid"] = list(
|
|
634
|
+
missmatch_summary[i]["missmatch_fullid"].keys()
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
# Save results to JSON if requested
|
|
638
|
+
if out_json is not None:
|
|
639
|
+
json_path = os.path.dirname(out_json)
|
|
640
|
+
if not os.path.isdir(json_path):
|
|
641
|
+
# Raise an error if the directory does not exist
|
|
642
|
+
raise FileNotFoundError(f"The directory {json_path} does not exist.")
|
|
643
|
+
|
|
644
|
+
cltmisc.save_dictionary_to_json(missmatch_summary, out_json)
|
|
645
|
+
|
|
646
|
+
return missmatch_summary, out_json
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
####################################################################################################
|
|
650
|
+
def get_processing_status_details_sqlite3(
|
|
651
|
+
proc_status_df: Union[str, dict],
|
|
652
|
+
subj_ids: Union[List[str], str],
|
|
653
|
+
deriv_dir: str,
|
|
654
|
+
pipe_dirs: Union[List[str], str] = None,
|
|
655
|
+
out_json: str = None,
|
|
656
|
+
db_path: str = None,
|
|
657
|
+
only_ids: bool = False,
|
|
658
|
+
):
|
|
659
|
+
"""
|
|
660
|
+
This function creates a dictionary with the details of the processing status of the subjects in the BIDs derivatives directory.
|
|
661
|
+
It provides the IDs of the subjects with incomplete or mismatched number of files.
|
|
662
|
+
|
|
663
|
+
Parameters
|
|
664
|
+
----------
|
|
665
|
+
proc_status_df : str or dict
|
|
666
|
+
Path to the processing status DataFrame or a DataFrame itself. This DataFrame can be
|
|
667
|
+
obtained with the function "create_processing_status_table".
|
|
668
|
+
|
|
669
|
+
subj_ids : list or str
|
|
670
|
+
List of subject IDs or a text file containing the subject IDs.
|
|
671
|
+
|
|
672
|
+
deriv_dir : str
|
|
673
|
+
Path to the derivatives directory.
|
|
674
|
+
|
|
675
|
+
pipe_dirs : list or str, optional
|
|
676
|
+
List of processing pipelines to check. If None, all pipelines will be checked.
|
|
677
|
+
|
|
678
|
+
out_json : str, optional
|
|
679
|
+
Path to save the output JSON file. If None, the JSON file will not be saved.
|
|
680
|
+
|
|
681
|
+
db_path : str, optional
|
|
682
|
+
Path to save the SQLite database file. If None, the database will not be created.
|
|
683
|
+
|
|
684
|
+
only_ids : bool, optional
|
|
685
|
+
If True, only the IDs of the subjects with mismatches will be returned, without the file details.
|
|
686
|
+
|
|
687
|
+
Returns
|
|
688
|
+
-------
|
|
689
|
+
dict
|
|
690
|
+
Dictionary containing the details of the processing status of the subjects.
|
|
691
|
+
|
|
692
|
+
str
|
|
693
|
+
Path to the saved JSON file if out_json is provided, otherwise None.
|
|
694
|
+
"""
|
|
695
|
+
|
|
696
|
+
from . import morphometrytools as cltmorpho
|
|
697
|
+
import sqlite3
|
|
698
|
+
|
|
699
|
+
if isinstance(proc_status_df, str):
|
|
700
|
+
if not os.path.isfile(proc_status_df):
|
|
701
|
+
raise FileNotFoundError(f"The file {proc_status_df} does not exist.")
|
|
702
|
+
else:
|
|
703
|
+
proc_status_df = cltmisc.smart_read_table(proc_status_df)
|
|
704
|
+
elif not isinstance(proc_status_df, pd.DataFrame):
|
|
705
|
+
raise TypeError("proc_status_df must be a DataFrame or a string path to a file")
|
|
706
|
+
|
|
707
|
+
# Process subject IDs
|
|
708
|
+
if isinstance(subj_ids, str):
|
|
709
|
+
if not os.path.isfile(subj_ids):
|
|
710
|
+
raise FileNotFoundError(f"The file {subj_ids} does not exist.")
|
|
711
|
+
else:
|
|
712
|
+
with open(subj_ids, "r") as f:
|
|
713
|
+
subj_list = f.read().splitlines()
|
|
714
|
+
elif isinstance(subj_ids, list):
|
|
715
|
+
if len(subj_ids) == 0:
|
|
716
|
+
raise ValueError("The list of subject IDs is empty.")
|
|
717
|
+
else:
|
|
718
|
+
subj_list = subj_ids
|
|
719
|
+
else:
|
|
720
|
+
raise TypeError("subj_ids must be a list or a string path to a file")
|
|
721
|
+
|
|
722
|
+
# Check if the derivatives directory exists
|
|
723
|
+
deriv_dir = cltmisc.remove_trailing_separators(deriv_dir)
|
|
724
|
+
|
|
725
|
+
if not os.path.isdir(deriv_dir):
|
|
726
|
+
raise FileNotFoundError(
|
|
727
|
+
f"The derivatives directory {deriv_dir} does not exist."
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
# Find all the derivatives folders
|
|
731
|
+
all_pipe_dirs = cltbids.get_derivatives_folders(deriv_dir)
|
|
732
|
+
|
|
733
|
+
if len(all_pipe_dirs) == 0:
|
|
734
|
+
raise ValueError(
|
|
735
|
+
"No derivatives folders were found in the specified directory."
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
if pipe_dirs is not None:
|
|
739
|
+
if isinstance(pipe_dirs, str):
|
|
740
|
+
pipe_dirs = [pipe_dirs]
|
|
741
|
+
|
|
742
|
+
pipe_dirs = cltmisc.filter_by_substring(all_pipe_dirs, or_filter=pipe_dirs)
|
|
743
|
+
else:
|
|
744
|
+
pipe_dirs = all_pipe_dirs
|
|
745
|
+
|
|
746
|
+
# All entities
|
|
747
|
+
ent_list = cltbids.entities4table()
|
|
748
|
+
|
|
749
|
+
# Get all the columns names
|
|
750
|
+
col_names = proc_status_df.columns.tolist()
|
|
751
|
+
|
|
752
|
+
# Get all the columns that are not in the pipe_dirs
|
|
753
|
+
subj_columns = list(set(col_names) - set(pipe_dirs))
|
|
754
|
+
|
|
755
|
+
subj_ids_df = proc_status_df[subj_columns]
|
|
756
|
+
|
|
757
|
+
# Create a consistent structure for the output dictionary
|
|
758
|
+
missmatch_summary = {}
|
|
759
|
+
|
|
760
|
+
# Initialize SQLite database if db_path is provided
|
|
761
|
+
if db_path:
|
|
762
|
+
conn = sqlite3.connect(db_path)
|
|
763
|
+
cursor = conn.cursor()
|
|
764
|
+
|
|
765
|
+
# Create tables
|
|
766
|
+
cursor.execute(
|
|
767
|
+
"""
|
|
768
|
+
CREATE TABLE IF NOT EXISTS pipelines (
|
|
769
|
+
pipeline_id TEXT PRIMARY KEY,
|
|
770
|
+
ref_fullid TEXT
|
|
771
|
+
)"""
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
cursor.execute(
|
|
775
|
+
"""
|
|
776
|
+
CREATE TABLE IF NOT EXISTS mismatches (
|
|
777
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
778
|
+
pipeline_id TEXT,
|
|
779
|
+
subject_id TEXT,
|
|
780
|
+
FOREIGN KEY (pipeline_id) REFERENCES pipelines(pipeline_id),
|
|
781
|
+
UNIQUE (pipeline_id, subject_id)
|
|
782
|
+
)"""
|
|
783
|
+
)
|
|
784
|
+
|
|
785
|
+
cursor.execute(
|
|
786
|
+
"""
|
|
787
|
+
CREATE TABLE IF NOT EXISTS file_details (
|
|
788
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
789
|
+
mismatch_id INTEGER,
|
|
790
|
+
file_path TEXT,
|
|
791
|
+
status TEXT,
|
|
792
|
+
FOREIGN KEY (mismatch_id) REFERENCES mismatches(id)
|
|
793
|
+
)"""
|
|
794
|
+
)
|
|
795
|
+
|
|
796
|
+
# Clear existing data if needed
|
|
797
|
+
cursor.execute("DELETE FROM file_details")
|
|
798
|
+
cursor.execute("DELETE FROM mismatches")
|
|
799
|
+
cursor.execute("DELETE FROM pipelines")
|
|
800
|
+
|
|
801
|
+
# Process each pipeline
|
|
802
|
+
for i in pipe_dirs:
|
|
803
|
+
proc_status_df[i] = proc_status_df[i].astype(int)
|
|
804
|
+
pipe_dir_fold = os.path.join(deriv_dir, i)
|
|
805
|
+
|
|
806
|
+
# Initialize consistent structure
|
|
807
|
+
missmatch_pipe = {"ref_fullid": "", "missmatch_fullid": {}}
|
|
808
|
+
|
|
809
|
+
# Get the mode for the column to determine the reference value
|
|
810
|
+
mode_value = proc_status_df[i].mode()[0]
|
|
811
|
+
|
|
812
|
+
# Find rows that match the mode (will be used as reference)
|
|
813
|
+
agreement_rows = proc_status_df[proc_status_df[i] == mode_value].index
|
|
814
|
+
|
|
815
|
+
# Get reference subject details (using the first row that matches the mode)
|
|
816
|
+
ref_ids = subj_ids_df.loc[agreement_rows].iloc[0, :]
|
|
817
|
+
|
|
818
|
+
# Create identifiers for the reference subject
|
|
819
|
+
cad2look_ref = [
|
|
820
|
+
f"{key}-{ref_ids[value]}"
|
|
821
|
+
for key, value in ent_list.items()
|
|
822
|
+
if value in subj_columns
|
|
823
|
+
]
|
|
824
|
+
|
|
825
|
+
# Get files for the reference subject
|
|
826
|
+
ref_files = cltbids.get_individual_files_and_folders(
|
|
827
|
+
pipe_dir_fold,
|
|
828
|
+
cad2look_ref,
|
|
829
|
+
)
|
|
830
|
+
|
|
831
|
+
# Find the full ID of the reference subject
|
|
832
|
+
try:
|
|
833
|
+
ref_full_id = cltmisc.filter_by_substring(
|
|
834
|
+
subj_list, or_filter=cad2look_ref[0], and_filter=cad2look_ref
|
|
835
|
+
)[0]
|
|
836
|
+
except IndexError:
|
|
837
|
+
# Handle case where reference ID is not found
|
|
838
|
+
ref_full_id = "unknown_reference"
|
|
839
|
+
|
|
840
|
+
missmatch_pipe["ref_fullid"] = ref_full_id
|
|
841
|
+
|
|
842
|
+
# If using SQLite, insert pipeline info
|
|
843
|
+
if db_path:
|
|
844
|
+
cursor.execute(
|
|
845
|
+
"INSERT OR REPLACE INTO pipelines VALUES (?, ?)", (i, ref_full_id)
|
|
846
|
+
)
|
|
847
|
+
|
|
848
|
+
# Find rows that don't match the mode (disagreement rows)
|
|
849
|
+
disagreement_rows = proc_status_df[proc_status_df[i] != mode_value].index
|
|
850
|
+
|
|
851
|
+
# Only process mismatches if reference files exist and there are disagreements
|
|
852
|
+
if ref_files and len(disagreement_rows) > 0:
|
|
853
|
+
# Process reference files to remove path prefixes for comparison
|
|
854
|
+
cad2look_ref.append(pipe_dir_fold)
|
|
855
|
+
tmp_ref_files = cltmisc.remove_substrings(ref_files, cad2look_ref)
|
|
856
|
+
|
|
857
|
+
# Get the ids of the subjects with disagreement
|
|
858
|
+
subtable_ids = subj_ids_df.loc[disagreement_rows]
|
|
859
|
+
|
|
860
|
+
# Loop through all subjects with disagreement
|
|
861
|
+
for j in range(len(disagreement_rows)):
|
|
862
|
+
# Get the subject ID
|
|
863
|
+
sub_row = subtable_ids.iloc[j, :]
|
|
864
|
+
|
|
865
|
+
# Create identifiers for this subject
|
|
866
|
+
cad2look_ind = [
|
|
867
|
+
f"{key}-{sub_row[value]}"
|
|
868
|
+
for key, value in ent_list.items()
|
|
869
|
+
if value in subj_columns
|
|
870
|
+
]
|
|
871
|
+
|
|
872
|
+
# Get files for this subject
|
|
873
|
+
indiv_files = cltbids.get_individual_files_and_folders(
|
|
874
|
+
pipe_dir_fold,
|
|
875
|
+
cad2look_ind,
|
|
876
|
+
)
|
|
877
|
+
|
|
878
|
+
try:
|
|
879
|
+
# Find the full ID of this subject
|
|
880
|
+
indiv_full_id = cltmisc.filter_by_substring(
|
|
881
|
+
subj_list,
|
|
882
|
+
or_filter=cad2look_ind[0],
|
|
883
|
+
and_filter=cad2look_ind,
|
|
884
|
+
)[0]
|
|
885
|
+
except IndexError:
|
|
886
|
+
# Handle case where subject ID is not found
|
|
887
|
+
indiv_full_id = f"unknown_subject_{j}"
|
|
888
|
+
|
|
889
|
+
# Initialize results for this subject
|
|
890
|
+
missmatch_subject = {"missing_files": [], "extra_files": []}
|
|
891
|
+
|
|
892
|
+
# Insert subject into mismatches table if using SQLite
|
|
893
|
+
if db_path:
|
|
894
|
+
cursor.execute(
|
|
895
|
+
"INSERT OR REPLACE INTO mismatches (pipeline_id, subject_id) VALUES (?, ?)",
|
|
896
|
+
(i, indiv_full_id),
|
|
897
|
+
)
|
|
898
|
+
mismatch_id = cursor.lastrowid
|
|
899
|
+
|
|
900
|
+
if indiv_files:
|
|
901
|
+
# Process individual files to remove path prefixes for comparison
|
|
902
|
+
cad2look_ind.append(pipe_dir_fold)
|
|
903
|
+
tmp_indiv_files = cltmisc.remove_substrings(
|
|
904
|
+
indiv_files, cad2look_ind
|
|
905
|
+
)
|
|
906
|
+
|
|
907
|
+
# Find missing files (in reference but not in this subject)
|
|
908
|
+
tmp_miss = list(set(tmp_ref_files) - set(tmp_indiv_files))
|
|
909
|
+
if tmp_miss:
|
|
910
|
+
miss_indices = cltmisc.get_indexes_by_substring(
|
|
911
|
+
tmp_ref_files, tmp_miss
|
|
912
|
+
)
|
|
913
|
+
selected_files_ref = [ref_files[i] for i in miss_indices]
|
|
914
|
+
missing_files = cltmisc.replace_substrings(
|
|
915
|
+
selected_files_ref, cad2look_ref, cad2look_ind
|
|
916
|
+
)
|
|
917
|
+
missmatch_subject["missing_files"] = missing_files
|
|
918
|
+
|
|
919
|
+
# Insert missing files into database if using SQLite
|
|
920
|
+
if db_path:
|
|
921
|
+
for file_path in missing_files:
|
|
922
|
+
cursor.execute(
|
|
923
|
+
"INSERT INTO file_details (mismatch_id, file_path, status) VALUES (?, ?, ?)",
|
|
924
|
+
(mismatch_id, file_path, "missing"),
|
|
925
|
+
)
|
|
926
|
+
|
|
927
|
+
# Find extra files (in this subject but not in reference)
|
|
928
|
+
tmp_extra = list(set(tmp_indiv_files) - set(tmp_ref_files))
|
|
929
|
+
if tmp_extra:
|
|
930
|
+
extra_indices = cltmisc.get_indexes_by_substring(
|
|
931
|
+
tmp_indiv_files, tmp_extra
|
|
932
|
+
)
|
|
933
|
+
selected_files_indiv = [indiv_files[i] for i in extra_indices]
|
|
934
|
+
extra_files = cltmisc.replace_substrings(
|
|
935
|
+
selected_files_indiv, cad2look_ind, cad2look_ref
|
|
936
|
+
)
|
|
937
|
+
missmatch_subject["extra_files"] = extra_files
|
|
938
|
+
|
|
939
|
+
# Insert extra files into database if using SQLite
|
|
940
|
+
if db_path:
|
|
941
|
+
for file_path in extra_files:
|
|
942
|
+
cursor.execute(
|
|
943
|
+
"INSERT INTO file_details (mismatch_id, file_path, status) VALUES (?, ?, ?)",
|
|
944
|
+
(mismatch_id, file_path, "extra"),
|
|
945
|
+
)
|
|
946
|
+
else:
|
|
947
|
+
# If no files found for this subject, all reference files are missing
|
|
948
|
+
missing_files = cltmisc.replace_substrings(
|
|
949
|
+
ref_files, cad2look_ref, cad2look_ind
|
|
950
|
+
)
|
|
951
|
+
missmatch_subject["missing_files"] = missing_files
|
|
952
|
+
|
|
953
|
+
# Insert missing files into database if using SQLite
|
|
954
|
+
if db_path:
|
|
955
|
+
for file_path in missing_files:
|
|
956
|
+
cursor.execute(
|
|
957
|
+
"INSERT INTO file_details (mismatch_id, file_path, status) VALUES (?, ?, ?)",
|
|
958
|
+
(mismatch_id, file_path, "missing"),
|
|
959
|
+
)
|
|
960
|
+
|
|
961
|
+
# Add this subject's details to the results
|
|
962
|
+
missmatch_pipe["missmatch_fullid"][indiv_full_id] = missmatch_subject
|
|
963
|
+
|
|
964
|
+
# Add this pipeline's results to the summary
|
|
965
|
+
missmatch_summary[i] = missmatch_pipe
|
|
966
|
+
|
|
967
|
+
# If only_ids is True, simplify the output to just include IDs
|
|
968
|
+
if only_ids:
|
|
969
|
+
for i in missmatch_summary.keys():
|
|
970
|
+
missmatch_summary[i]["missmatch_fullid"] = list(
|
|
971
|
+
missmatch_summary[i]["missmatch_fullid"].keys()
|
|
972
|
+
)
|
|
973
|
+
|
|
974
|
+
# Commit changes and close database connection if using SQLite
|
|
975
|
+
if db_path:
|
|
976
|
+
conn.commit()
|
|
977
|
+
conn.close()
|
|
978
|
+
|
|
979
|
+
# Save results to JSON if requested
|
|
980
|
+
if out_json is not None:
|
|
981
|
+
json_path = os.path.dirname(out_json)
|
|
982
|
+
if not os.path.isdir(json_path):
|
|
983
|
+
# Raise an error if the directory does not exist
|
|
984
|
+
raise FileNotFoundError(f"The directory {json_path} does not exist.")
|
|
985
|
+
|
|
986
|
+
cltmisc.save_dictionary_to_json(missmatch_summary, out_json)
|
|
987
|
+
|
|
988
|
+
return missmatch_summary, out_json
|
|
989
|
+
|
|
990
|
+
|
|
991
|
+
####################################################################################################
|
|
992
|
+
def query_processing_status_db(
|
|
993
|
+
db_path, query_type="subjects_with_mismatches", pipeline=None
|
|
994
|
+
):
|
|
995
|
+
"""
|
|
996
|
+
Query the processing status database to extract useful information.
|
|
997
|
+
|
|
998
|
+
Parameters
|
|
999
|
+
----------
|
|
1000
|
+
db_path : str
|
|
1001
|
+
Path to the SQLite database file.
|
|
1002
|
+
|
|
1003
|
+
query_type : str, optional
|
|
1004
|
+
Type of query to run. Options:
|
|
1005
|
+
- "subjects_with_mismatches": Get all subjects with mismatches
|
|
1006
|
+
- "pipelines_with_mismatches": Get all pipelines with mismatches and count
|
|
1007
|
+
- "missing_files_count": Get number of missing files per subject
|
|
1008
|
+
- "extra_files_count": Get number of extra files per subject
|
|
1009
|
+
|
|
1010
|
+
pipeline : str, optional
|
|
1011
|
+
Name of the pipeline to filter by. Used only with certain query types.
|
|
1012
|
+
|
|
1013
|
+
Returns
|
|
1014
|
+
-------
|
|
1015
|
+
pd.DataFrame
|
|
1016
|
+
Result of the query as a DataFrame.
|
|
1017
|
+
"""
|
|
1018
|
+
import sqlite3
|
|
1019
|
+
import pandas as pd
|
|
1020
|
+
|
|
1021
|
+
conn = sqlite3.connect(db_path)
|
|
1022
|
+
|
|
1023
|
+
if query_type == "subjects_with_mismatches":
|
|
1024
|
+
if pipeline:
|
|
1025
|
+
query = """
|
|
1026
|
+
SELECT subject_id, pipeline_id
|
|
1027
|
+
FROM mismatches
|
|
1028
|
+
WHERE pipeline_id = ?
|
|
1029
|
+
ORDER BY subject_id
|
|
1030
|
+
"""
|
|
1031
|
+
df = pd.read_sql_query(query, conn, params=(pipeline,))
|
|
1032
|
+
else:
|
|
1033
|
+
query = """
|
|
1034
|
+
SELECT subject_id, GROUP_CONCAT(pipeline_id) as pipelines
|
|
1035
|
+
FROM mismatches
|
|
1036
|
+
GROUP BY subject_id
|
|
1037
|
+
ORDER BY subject_id
|
|
1038
|
+
"""
|
|
1039
|
+
df = pd.read_sql_query(query, conn)
|
|
1040
|
+
|
|
1041
|
+
elif query_type == "pipelines_with_mismatches":
|
|
1042
|
+
query = """
|
|
1043
|
+
SELECT pipeline_id, COUNT(DISTINCT subject_id) as subject_count
|
|
1044
|
+
FROM mismatches
|
|
1045
|
+
GROUP BY pipeline_id
|
|
1046
|
+
ORDER BY subject_count DESC
|
|
1047
|
+
"""
|
|
1048
|
+
df = pd.read_sql_query(query, conn)
|
|
1049
|
+
|
|
1050
|
+
elif query_type == "missing_files_count":
|
|
1051
|
+
if pipeline:
|
|
1052
|
+
query = """
|
|
1053
|
+
SELECT m.subject_id, COUNT(*) as missing_count
|
|
1054
|
+
FROM mismatches m
|
|
1055
|
+
JOIN file_details f ON m.id = f.mismatch_id
|
|
1056
|
+
WHERE f.status = 'missing' AND m.pipeline_id = ?
|
|
1057
|
+
GROUP BY m.subject_id
|
|
1058
|
+
ORDER BY missing_count DESC
|
|
1059
|
+
"""
|
|
1060
|
+
df = pd.read_sql_query(query, conn, params=(pipeline,))
|
|
1061
|
+
else:
|
|
1062
|
+
query = """
|
|
1063
|
+
SELECT m.subject_id, m.pipeline_id, COUNT(*) as missing_count
|
|
1064
|
+
FROM mismatches m
|
|
1065
|
+
JOIN file_details f ON m.id = f.mismatch_id
|
|
1066
|
+
WHERE f.status = 'missing'
|
|
1067
|
+
GROUP BY m.subject_id, m.pipeline_id
|
|
1068
|
+
ORDER BY missing_count DESC
|
|
1069
|
+
"""
|
|
1070
|
+
df = pd.read_sql_query(query, conn)
|
|
1071
|
+
|
|
1072
|
+
elif query_type == "extra_files_count":
|
|
1073
|
+
if pipeline:
|
|
1074
|
+
query = """
|
|
1075
|
+
SELECT m.subject_id, COUNT(*) as extra_count
|
|
1076
|
+
FROM mismatches m
|
|
1077
|
+
JOIN file_details f ON m.id = f.mismatch_id
|
|
1078
|
+
WHERE f.status = 'extra' AND m.pipeline_id = ?
|
|
1079
|
+
GROUP BY m.subject_id
|
|
1080
|
+
ORDER BY extra_count DESC
|
|
1081
|
+
"""
|
|
1082
|
+
df = pd.read_sql_query(query, conn, params=(pipeline,))
|
|
1083
|
+
else:
|
|
1084
|
+
query = """
|
|
1085
|
+
SELECT m.subject_id, m.pipeline_id, COUNT(*) as extra_count
|
|
1086
|
+
FROM mismatches m
|
|
1087
|
+
JOIN file_details f ON m.id = f.mismatch_id
|
|
1088
|
+
WHERE f.status = 'extra'
|
|
1089
|
+
GROUP BY m.subject_id, m.pipeline_id
|
|
1090
|
+
ORDER BY extra_count DESC
|
|
1091
|
+
"""
|
|
1092
|
+
df = pd.read_sql_query(query, conn)
|
|
1093
|
+
|
|
1094
|
+
else:
|
|
1095
|
+
raise ValueError(f"Unknown query type: {query_type}")
|
|
1096
|
+
|
|
1097
|
+
conn.close()
|
|
1098
|
+
return df
|
|
1099
|
+
|
|
1100
|
+
|
|
1101
|
+
####################################################################################################
|
|
1102
|
+
def export_db_to_json(db_path, out_json):
|
|
1103
|
+
"""
|
|
1104
|
+
Export the processing status database to a JSON file in the same format
|
|
1105
|
+
as returned by get_processing_status_details.
|
|
1106
|
+
|
|
1107
|
+
Parameters
|
|
1108
|
+
----------
|
|
1109
|
+
db_path : str
|
|
1110
|
+
Path to the SQLite database file.
|
|
1111
|
+
|
|
1112
|
+
out_json : str
|
|
1113
|
+
Path to save the output JSON file.
|
|
1114
|
+
|
|
1115
|
+
Returns
|
|
1116
|
+
-------
|
|
1117
|
+
dict
|
|
1118
|
+
Dictionary containing the details of the processing status of the subjects.
|
|
1119
|
+
"""
|
|
1120
|
+
import sqlite3
|
|
1121
|
+
import json
|
|
1122
|
+
import os
|
|
1123
|
+
|
|
1124
|
+
conn = sqlite3.connect(db_path)
|
|
1125
|
+
cursor = conn.cursor()
|
|
1126
|
+
|
|
1127
|
+
# Get all pipelines
|
|
1128
|
+
cursor.execute("SELECT pipeline_id, ref_fullid FROM pipelines")
|
|
1129
|
+
pipelines = cursor.fetchall()
|
|
1130
|
+
|
|
1131
|
+
# Create the output dictionary
|
|
1132
|
+
output_dict = {}
|
|
1133
|
+
|
|
1134
|
+
for pipe_id, ref_fullid in pipelines:
|
|
1135
|
+
# Initialize pipeline entry
|
|
1136
|
+
pipe_entry = {"ref_fullid": ref_fullid, "missmatch_fullid": {}}
|
|
1137
|
+
|
|
1138
|
+
# Get all mismatches for this pipeline
|
|
1139
|
+
cursor.execute(
|
|
1140
|
+
"""
|
|
1141
|
+
SELECT id, subject_id
|
|
1142
|
+
FROM mismatches
|
|
1143
|
+
WHERE pipeline_id = ?
|
|
1144
|
+
""",
|
|
1145
|
+
(pipe_id,),
|
|
1146
|
+
)
|
|
1147
|
+
mismatches = cursor.fetchall()
|
|
1148
|
+
|
|
1149
|
+
for mismatch_id, subject_id in mismatches:
|
|
1150
|
+
# Get missing files
|
|
1151
|
+
cursor.execute(
|
|
1152
|
+
"""
|
|
1153
|
+
SELECT file_path
|
|
1154
|
+
FROM file_details
|
|
1155
|
+
WHERE mismatch_id = ? AND status = 'missing'
|
|
1156
|
+
""",
|
|
1157
|
+
(mismatch_id,),
|
|
1158
|
+
)
|
|
1159
|
+
missing_files = [row[0] for row in cursor.fetchall()]
|
|
1160
|
+
|
|
1161
|
+
# Get extra files
|
|
1162
|
+
cursor.execute(
|
|
1163
|
+
"""
|
|
1164
|
+
SELECT file_path
|
|
1165
|
+
FROM file_details
|
|
1166
|
+
WHERE mismatch_id = ? AND status = 'extra'
|
|
1167
|
+
""",
|
|
1168
|
+
(mismatch_id,),
|
|
1169
|
+
)
|
|
1170
|
+
extra_files = [row[0] for row in cursor.fetchall()]
|
|
1171
|
+
|
|
1172
|
+
# Add to dictionary
|
|
1173
|
+
pipe_entry["missmatch_fullid"][subject_id] = {
|
|
1174
|
+
"missing_files": missing_files,
|
|
1175
|
+
"extra_files": extra_files,
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
# Add pipeline to output
|
|
1179
|
+
output_dict[pipe_id] = pipe_entry
|
|
1180
|
+
|
|
1181
|
+
conn.close()
|
|
1182
|
+
|
|
1183
|
+
# Save to JSON
|
|
1184
|
+
with open(out_json, "w") as f:
|
|
1185
|
+
json.dump(output_dict, f, indent=2)
|
|
1186
|
+
|
|
1187
|
+
return output_dict
|