Skip to content

Commit a1c363b

Browse files
authored
Merge branch 'develop' into lite-bc
2 parents ed7644b + d0340d1 commit a1c363b

File tree

11 files changed

+664
-135
lines changed

11 files changed

+664
-135
lines changed

.github/Dockerfiles/base-standard.Dockerfile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ Standard software dependencies for C-PAC standard images"
2222
LABEL org.opencontainers.image.source=https://github.com/FCP-INDI/C-PAC
2323
USER root
2424

25+
# Installing ANTs
26+
ENV LANG="en_US.UTF-8" \
27+
LC_ALL="en_US.UTF-8" \
28+
ANTSPATH=/usr/lib/ants/bin \
29+
PATH=/usr/lib/ants/bin:$PATH
30+
2531
# Installing FreeSurfer
2632
RUN apt-get update \
2733
&& yes | mamba install tcsh \

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7070
- Lingering calls to `cpac_outputs.csv` (was changed to `cpac_outputs.tsv` in v1.8.1).
7171
- A bug in the `freesurfer_abcd_preproc` nodeblock where the `Template` image was incorrectly used as `reference` during the `inverse_warp` step. Replacing it with the subject-specific `T1w` image resolved the issue of the `desc-restoreBrain_T1w` being chipped off.
7272
- A bug in `ideal_bandpass` where the frequency mask was incorrectly applied, which caused filter to fail in certain cases.
73+
- A bug where `$ANTSPATH` was unset in C-PAC with FreeSurfer images.
7374

7475
### Upgraded dependencies
7576

CPAC/distortion_correction/distortion_correction.py

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
from CPAC.pipeline import nipype_pipeline_engine as pe
3737
from CPAC.pipeline.nodeblock import nodeblock
3838
from CPAC.utils import function
39-
from CPAC.utils.datasource import match_epi_fmaps
39+
from CPAC.utils.datasource import match_epi_fmaps_function_node
4040
from CPAC.utils.interfaces.function import Function
4141

4242

@@ -406,23 +406,7 @@ def distcor_blip_afni_qwarp(wf, cfg, strat_pool, pipe_num, opt=None):
406406
3dQWarp. The output of this can then proceed to
407407
func_preproc.
408408
"""
409-
match_epi_imports = ["import json"]
410-
match_epi_fmaps_node = pe.Node(
411-
Function(
412-
input_names=[
413-
"bold_pedir",
414-
"epi_fmap_one",
415-
"epi_fmap_params_one",
416-
"epi_fmap_two",
417-
"epi_fmap_params_two",
418-
],
419-
output_names=["opposite_pe_epi", "same_pe_epi"],
420-
function=match_epi_fmaps,
421-
imports=match_epi_imports,
422-
as_module=True,
423-
),
424-
name=f"match_epi_fmaps_{pipe_num}",
425-
)
409+
match_epi_fmaps_node = match_epi_fmaps_function_node(f"match_epi_fmaps_{pipe_num}")
426410

427411
node, out = strat_pool.get_data("epi-1")
428412
wf.connect(node, out, match_epi_fmaps_node, "epi_fmap_one")

CPAC/seg_preproc/tests/__init__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Copyright (C) 2025 C-PAC Developers
2+
3+
# This file is part of C-PAC.
4+
5+
# C-PAC is free software: you can redistribute it and/or modify it under
6+
# the terms of the GNU Lesser General Public License as published by the
7+
# Free Software Foundation, either version 3 of the License, or (at your
8+
# option) any later version.
9+
10+
# C-PAC is distributed in the hope that it will be useful, but WITHOUT
11+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
13+
# License for more details.
14+
15+
# You should have received a copy of the GNU Lesser General Public
16+
# License along with C-PAC. If not, see <https://www.gnu.org/licenses/>.
17+
"""Tests for segmentation utilities."""
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Copyright (C) 2025 C-PAC Developers
2+
3+
# This file is part of C-PAC.
4+
5+
# C-PAC is free software: you can redistribute it and/or modify it under
6+
# the terms of the GNU Lesser General Public License as published by the
7+
# Free Software Foundation, either version 3 of the License, or (at your
8+
# option) any later version.
9+
10+
# C-PAC is distributed in the hope that it will be useful, but WITHOUT
11+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
13+
# License for more details.
14+
15+
# You should have received a copy of the GNU Lesser General Public
16+
# License along with C-PAC. If not, see <https://www.gnu.org/licenses/>.
17+
"""Tests for segmentation utilities."""
18+
19+
import subprocess
20+
21+
22+
def test_ants_joint_label_fusion_script() -> None:
23+
"""Test antsJointLabelFusion.sh script can run in this environment."""
24+
try:
25+
subprocess.run(
26+
["antsJointLabelFusion.sh"],
27+
check=True,
28+
capture_output=True,
29+
)
30+
except subprocess.CalledProcessError as e:
31+
# There's no explicit 'help' option, but if the script can run,
32+
# the error message does not contain the string "Error".
33+
if "Error" in e.stderr.decode():
34+
raise e

CPAC/utils/datasource.py

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (C) 2012-2024 C-PAC Developers
1+
# Copyright (C) 2012-2025 C-PAC Developers
22

33
# This file is part of C-PAC.
44

@@ -20,6 +20,7 @@
2020
import json
2121
from pathlib import Path
2222
import re
23+
from typing import Any, Optional
2324

2425
from voluptuous import RequiredFieldInvalid
2526
from nipype.interfaces import utility as util
@@ -463,12 +464,12 @@ def gather_echo_times(echotime_1, echotime_2, echotime_3=None, echotime_4=None):
463464

464465

465466
def match_epi_fmaps(
466-
bold_pedir,
467-
epi_fmap_one,
468-
epi_fmap_params_one,
469-
epi_fmap_two=None,
470-
epi_fmap_params_two=None,
471-
):
467+
bold_pedir: str,
468+
epi_fmap_one: str,
469+
epi_fmap_params_one: dict[str, Any],
470+
epi_fmap_two: Optional[str] = None,
471+
epi_fmap_params_two: Optional[dict[str, Any]] = None,
472+
) -> tuple[str, str]:
472473
"""Match EPI field maps to the BOLD scan.
473474
474475
Parse the field map files in the data configuration and determine which
@@ -504,13 +505,41 @@ def match_epi_fmaps(
504505
with open(scan_params, "r") as f:
505506
scan_params = json.load(f)
506507
if "PhaseEncodingDirection" in scan_params:
507-
epi_pedir = scan_params["PhaseEncodingDirection"]
508+
epi_pedir: str | bytes = scan_params["PhaseEncodingDirection"]
509+
if isinstance(epi_pedir, bytes):
510+
epi_pedir = epi_pedir.decode("utf-8")
508511
if epi_pedir == bold_pedir:
509512
same_pe_epi = epi_scan
510513
elif epi_pedir[0] == bold_pedir[0]:
511514
opposite_pe_epi = epi_scan
512515

513-
return (opposite_pe_epi, same_pe_epi)
516+
if same_pe_epi is None:
517+
msg = f"Same phase encoding EPI: {bold_pedir}"
518+
raise FileNotFoundError(msg)
519+
if opposite_pe_epi is None:
520+
msg = f"Opposite phase encoding EPI: {bold_pedir}"
521+
raise FileNotFoundError(msg)
522+
523+
return opposite_pe_epi, same_pe_epi
524+
525+
526+
def match_epi_fmaps_function_node(name: str = "match_epi_fmaps"):
527+
"""Return a Function node for `~CPAC.utils.datasource.match_epi_fmaps`."""
528+
return pe.Node(
529+
Function(
530+
input_names=[
531+
"bold_pedir",
532+
"epi_fmap_one",
533+
"epi_fmap_params_one",
534+
"epi_fmap_two",
535+
"epi_fmap_params_two",
536+
],
537+
output_names=["opposite_pe_epi", "same_pe_epi"],
538+
function=match_epi_fmaps,
539+
as_module=True,
540+
),
541+
name=name,
542+
)
514543

515544

516545
def ingress_func_metadata(

CPAC/utils/monitoring/draw_gantt_chart.py

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -39,20 +39,19 @@
3939

4040
# You should have received a copy of the GNU Lesser General Public
4141
# License along with C-PAC. If not, see <https://www.gnu.org/licenses/>.
42-
"""Module to draw an html gantt chart from logfile produced by
43-
``CPAC.utils.monitoring.log_nodes_cb()``.
42+
"""Module to draw an html gantt chart from logfile produced by `~CPAC.utils.monitoring.log_nodes_cb`.
4443
4544
See https://nipype.readthedocs.io/en/latest/api/generated/nipype.utils.draw_gantt_chart.html
4645
"""
4746

4847
from collections import OrderedDict
49-
from datetime import datetime
48+
from datetime import datetime, timedelta
5049
import random
5150
from warnings import warn
5251

5352
from nipype.utils.draw_gantt_chart import draw_lines, draw_resource_bar, log_to_dict
5453

55-
from CPAC.utils.monitoring.monitoring import DatetimeWithSafeNone
54+
from CPAC.utils.monitoring.monitoring import _NoTime, DatetimeWithSafeNone
5655

5756

5857
def create_event_dict(start_time, nodes_list):
@@ -404,37 +403,38 @@ def generate_gantt_chart(
404403

405404
for node in nodes_list:
406405
if "duration" not in node and (node["start"] and node["finish"]):
407-
node["duration"] = (node["finish"] - node["start"]).total_seconds()
406+
_duration = node["finish"] - node["start"]
407+
assert isinstance(_duration, timedelta)
408+
node["duration"] = _duration.total_seconds()
408409

409410
# Create the header of the report with useful information
410411
start_node = nodes_list[0]
411412
last_node = nodes_list[-1]
413+
start = DatetimeWithSafeNone(start_node["start"])
414+
finish = DatetimeWithSafeNone(last_node["finish"])
415+
if isinstance(start, _NoTime) or isinstance(finish, _NoTime):
416+
return
417+
start, finish = DatetimeWithSafeNone.sync_tz(start, finish)
412418
try:
413-
duration = (last_node["finish"] - start_node["start"]).total_seconds()
419+
duration = (finish - start).total_seconds()
414420
except TypeError:
415421
# no duration
416422
return
417423

418424
# Get events based dictionary of node run stats
419-
events = create_event_dict(start_node["start"], nodes_list)
425+
events = create_event_dict(start, nodes_list)
420426

421427
# Summary strings of workflow at top
422-
html_string += (
423-
"<p>Start: " + start_node["start"].strftime("%Y-%m-%d %H:%M:%S") + "</p>"
424-
)
425-
html_string += (
426-
"<p>Finish: " + last_node["finish"].strftime("%Y-%m-%d %H:%M:%S") + "</p>"
427-
)
428+
html_string += "<p>Start: " + start.strftime("%Y-%m-%d %H:%M:%S") + "</p>"
429+
html_string += "<p>Finish: " + finish.strftime("%Y-%m-%d %H:%M:%S") + "</p>"
428430
html_string += "<p>Duration: " + f"{duration / 60:.2f}" + " minutes</p>"
429431
html_string += "<p>Nodes: " + str(len(nodes_list)) + "</p>"
430432
html_string += "<p>Cores: " + str(cores) + "</p>"
431433
html_string += close_header
432434
# Draw nipype nodes Gantt chart and runtimes
433-
html_string += draw_lines(
434-
start_node["start"], duration, minute_scale, space_between_minutes
435-
)
435+
html_string += draw_lines(start, duration, minute_scale, space_between_minutes)
436436
html_string += draw_nodes(
437-
start_node["start"],
437+
start,
438438
nodes_list,
439439
cores,
440440
minute_scale,
@@ -448,8 +448,8 @@ def generate_gantt_chart(
448448
# Plot gantt chart
449449
resource_offset = 120 + 30 * cores
450450
html_string += draw_resource_bar(
451-
start_node["start"],
452-
last_node["finish"],
451+
start,
452+
finish,
453453
estimated_mem_ts,
454454
space_between_minutes,
455455
minute_scale,
@@ -458,8 +458,8 @@ def generate_gantt_chart(
458458
"Memory",
459459
)
460460
html_string += draw_resource_bar(
461-
start_node["start"],
462-
last_node["finish"],
461+
start,
462+
finish,
463463
runtime_mem_ts,
464464
space_between_minutes,
465465
minute_scale,
@@ -473,8 +473,8 @@ def generate_gantt_chart(
473473
runtime_threads_ts = calculate_resource_timeseries(events, "runtime_threads")
474474
# Plot gantt chart
475475
html_string += draw_resource_bar(
476-
start_node["start"],
477-
last_node["finish"],
476+
start,
477+
finish,
478478
estimated_threads_ts,
479479
space_between_minutes,
480480
minute_scale,
@@ -483,8 +483,8 @@ def generate_gantt_chart(
483483
"Threads",
484484
)
485485
html_string += draw_resource_bar(
486-
start_node["start"],
487-
last_node["finish"],
486+
start,
487+
finish,
488488
runtime_threads_ts,
489489
space_between_minutes,
490490
minute_scale,

0 commit comments

Comments
 (0)