simnibs_analyze._pipeline_io

SimNIBS pipeline I/O module. Centralises all input/output operations: file discovery, NIfTI image loading, CSV and YAML configuration reading/writing.

  1"""
  2SimNIBS pipeline I/O module.
  3Centralises all input/output operations: file discovery, NIfTI image loading,
  4CSV and YAML configuration reading/writing.
  5"""
  6
  7from __future__ import annotations
  8
  9from pathlib import Path
 10from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
 11
 12import nibabel as nib
 13import numpy as np
 14import pandas as pd
 15
 16from ._config import PipelineConfig
 17from ._logging import get_logger
 18
 19logger = get_logger(__name__)
 20
 21SPACE_MNI = "mni"
 22SPACE_NATIVE = "native"
 23
 24
 25# ---------------------------------------------------------------------------
 26# Output naming context
 27# ---------------------------------------------------------------------------
 28
 29
 30class OutputContext:
 31    """
 32    Groups all parameters needed to build pipeline output filenames.
 33    Instantiate in the main loop (run.py) and pass to save / get_*_path functions.
 34
 35    Parameters
 36    ----------
 37    subject : str
 38        Subject identifier (e.g. '0008').
 39    condition : str
 40        ROI / stimulation condition name (e.g. 'fef', 'ips-left').
 41        Used in preprocessed filenames and mask names.
 42    mode : str
 43        'simulation' or 'optimization'.
 44    space : str
 45        'mni' or 'native'.
 46    roi_name : str
 47        ROI key as defined in config.yaml → target_generation.rois.
 48        Usually identical to *condition*; useful to distinguish the config key
 49        from the filename.
 50    base_name : str or None
 51        Stem of the source e-field file (e.g. 'sub-0008_scalar_MNI_magnE').
 52        Required for get_preproc_paths.  Set after find_efield_files.
 53    results_dir : Path or None
 54        Root results directory (config.paths.results_dir).
 55    simnibs_output : Path or None
 56        Root SimNIBS output directory (config.paths.simnibs_output).
 57    """
 58
 59    def __init__(
 60        self,
 61        subject: str = "",
 62        condition: str = "",
 63        mode: str = "",
 64        space: str = SPACE_MNI,
 65        roi_name: str = "",
 66        base_name: str = "",
 67        results_dir: Optional[Path] = None,
 68        simnibs_output: Optional[Path] = None,
 69    ) -> None:
 70        self.subject = subject
 71        self.condition = condition
 72        self.mode = mode
 73        self.space = space
 74        self.roi_name = roi_name or condition  # default: same as condition
 75        self.base_name = base_name
 76        self.results_dir = results_dir
 77        self.simnibs_output = simnibs_output
 78
 79
 80def normalize_space(space: str) -> str:
 81    """Normalize and validate the working space value."""
 82    normalized = str(space).lower().strip()
 83    if normalized not in {SPACE_MNI, SPACE_NATIVE}:
 84        raise ValueError(
 85            f"Invalid 'space' parameter: {space}. Allowed values: {SPACE_MNI}, {SPACE_NATIVE}"
 86        )
 87    return normalized
 88
 89
 90def space_tag(space: str) -> str:
 91    """Return tagged space label used in output paths and file names."""
 92    return f"space-{normalize_space(space)}"
 93
 94
 95def get_preps_root(paths_config) -> Path:
 96    """Retourne la racine des segmentations charm (m2m_*)."""
 97    if paths_config.simnibs_preps is not None:
 98        return Path(paths_config.simnibs_preps)
 99    return Path(paths_config.simnibs_output)
100
101
102def get_simu_root(paths_config) -> Path:
103    """Retourne la racine des simulations/optimisations."""
104    if paths_config.simnibs_simu is not None:
105        return Path(paths_config.simnibs_simu)
106    return Path(paths_config.simnibs_output)
107
108
109def get_subject_paths(simnibs_output_dir: Path, subject: str) -> Dict[str, Path]:
110    """Return canonical subject-level paths used across the pipeline."""
111    subject_dir = Path(simnibs_output_dir) / subject
112    return {
113        "subject_dir": subject_dir,
114        "m2m_dir": subject_dir / f"m2m_{subject}",
115        "subject_target_dir": subject_dir / "subject_target",
116    }
117
118
119def get_subject_paths_from_config(paths_config, subject: str) -> Dict[str, Path]:
120    """
121    Return canonical subject-level paths, handling split preps/simu directories.
122
123    - ``subject_dir``        : simu root / subject  (simulations live here)
124    - ``m2m_dir``            : preps root / subject / m2m_{subject}  (charm segmentation)
125    - ``subject_target_dir`` : simu root / subject / subject_target
126    """
127    preps_root = get_preps_root(paths_config)
128    simu_root = get_simu_root(paths_config)
129    simu_subject_dir = simu_root / subject
130    preps_subject_dir = preps_root / subject
131    return {
132        "subject_dir": simu_subject_dir,
133        "m2m_dir": preps_subject_dir / f"m2m_{subject}",
134        "subject_target_dir": simu_subject_dir / "subject_target",
135    }
136
137
138def get_analysis_dir(results_dir: Path, space: str) -> Path:
139    """Return the shared analysis output directory."""
140    return Path(results_dir) / "3-analysis"
141
142
143def get_features_csv_path(results_dir: Path, space: str) -> Path:
144    """Return the canonical features CSV path for a given space."""
145    return get_analysis_dir(results_dir, space) / f"all_features_{space_tag(space)}.csv"
146
147
148def get_inter_subject_summary_csv_path(results_dir: Path, space: str) -> Path:
149    """Return the inter-subject summary CSV path for a given space."""
150    return (
151        get_analysis_dir(results_dir, space)
152        / f"inter_subject_summary_{space_tag(space)}.csv"
153    )
154
155
156def get_intra_subject_diff_csv_path(
157    results_dir: Path, space: str, condition: str
158) -> Path:
159    """Return the intra-subject diff CSV path for a condition and space."""
160    return (
161        get_analysis_dir(results_dir, space)
162        / f"intra_subject_diff_{condition}_{space_tag(space)}.csv"
163    )
164
165
166def get_clusters_csv_path(results_dir: Path, space: str) -> Path:
167    """Return the clustering CSV path for a given space."""
168    return get_analysis_dir(results_dir, space) / f"clusters_{space_tag(space)}.csv"
169
170
171def load_config(config_path: Path) -> PipelineConfig:
172    """Load and validate the YAML configuration file via Pydantic models."""
173    from _config import load_and_validate
174
175    return load_and_validate(config_path)
176
177
178def find_raw_efield(
179    simnibs_output_dir: Path, subject: str, roi: str, mode: str
180) -> Optional[Path]:
181    """
182    Find the raw (unprocessed) e-field file in the SimNIBS output directory.
183
184    Parameters
185    ----------
186    simnibs_output_dir : Path
187        SimNIBS output directory.
188    subject : str
189        Subject ID.
190    roi : str
191        ROI name.
192    mode : str
193        Mode (simulation or optimization).
194
195    Returns
196    -------
197    Path or None
198        Path to the file, or None if not found.
199    """
200    subject_dir = simnibs_output_dir / subject
201
202    if not subject_dir.exists():
203        return None
204
205    if mode == "simulation":
206        base_dir = subject_dir / "simulations"
207    else:
208        base_dir = subject_dir / "optimizations"
209
210    if not base_dir.exists():
211        return None
212
213    pattern = f"{mode}_{mode}_{roi}_*"
214    matching_dirs = list(base_dir.glob(pattern))
215
216    if not matching_dirs:
217        return None
218
219    mode_dir = matching_dirs[0]
220
221    if mode == "optimization":
222        mni_volumes_dir = mode_dir / "simulation_with_optimal_montage" / "mni_volumes"
223    else:
224        mni_volumes_dir = mode_dir / "mni_volumes"
225
226    if not mni_volumes_dir.exists():
227        return None
228
229    efield_files = list(mni_volumes_dir.glob("*_scalar_MNI_magnE.nii.gz"))
230    return efield_files[0] if efield_files else None
231
232
233def find_simulation_dirs(
234    subject_dir: Path, condition: str, mode: str, folder_pattern: str | None = None
235) -> List[Path]:
236    """
237    Find all simulation/optimization directories for a given condition.
238    Handles hashes in folder names.
239
240    Parameters
241    ----------
242    subject_dir : Path
243        Subject directory (e.g. 001-CC).
244    condition : str
245        Stimulation condition (e.g. fef, ips-left).
246        Used as the search fragment if *folder_pattern* is not provided.
247    mode : str
248        Mode (simulation or optimization).
249    folder_pattern : str or None
250        Glob fragment to use instead of the condition name when searching
251        SimNIBS folders.  Useful when the ROI name differs from the folder name
252        (e.g. ROI 'ips-left' but folders named '…ips_left…').
253
254    Returns
255    -------
256    List[Path]
257        List of matching directories.
258    """
259    fragment = folder_pattern if folder_pattern is not None else condition
260    pattern = f"*{mode}_{fragment}*"
261
262    if mode == "simulation":
263        base_dir = subject_dir / "simulations"
264    else:
265        base_dir = subject_dir / "optimizations"
266
267    if not base_dir.exists():
268        logger.warning(f"{mode} directory not found: {base_dir}")
269        return []
270
271    found_dirs = list(base_dir.glob(pattern))
272
273    if not found_dirs:
274        logger.warning(f"No {mode} found for pattern: {pattern} in {base_dir}")
275
276    return found_dirs
277
278
279def find_efield_files(
280    simulation_dir: Path, mode: str, space: str = SPACE_MNI
281) -> List[Path]:
282    """
283    Find e-field files in a simulation/optimization directory.
284
285    Parameters
286    ----------
287    simulation_dir : Path
288        Simulation or optimization directory.
289    mode : str
290        Mode (simulation or optimization).
291    space : str
292        ``'mni'`` (default): ``*_scalar_MNI_magnE.nii.gz`` files in ``mni_volumes/``.
293        ``'native'``: ``*_scalar_magnE.nii.gz`` files in ``subject_volumes/``.
294
295    Returns
296    -------
297    List[Path]
298        List of e-field files found.
299    """
300    space = normalize_space(space)
301
302    if space == SPACE_NATIVE:
303        if mode == "optimization":
304            volumes_dir = (
305                simulation_dir / "simulation_with_optimal_montage" / "subject_volumes"
306            )
307        else:
308            volumes_dir = simulation_dir / "subject_volumes"
309        glob_pattern = "*_scalar_magnE.nii.gz"
310    else:
311        if mode == "optimization":
312            volumes_dir = (
313                simulation_dir / "simulation_with_optimal_montage" / "mni_volumes"
314            )
315        else:
316            volumes_dir = simulation_dir / "mni_volumes"
317        glob_pattern = "*_scalar_MNI_magnE.nii.gz"
318
319    if not volumes_dir.exists():
320        logger.warning(f"{space}_volumes directory not found: {volumes_dir}")
321        return []
322
323    efield_files = list(volumes_dir.glob(glob_pattern))
324
325    if not efield_files:
326        logger.warning(f"No e-field files found in {volumes_dir}")
327
328    return efield_files
329
330
331def get_t1_conform(
332    m2m_dir: Path,
333    filename: str = "segmentation/T1_bias_corrected.nii.gz",
334) -> Path:
335    """
336    Return the path to the T1 file inside ``m2m_dir``.
337
338    Parameters
339    ----------
340    m2m_dir : Path
341        ``m2m_<subject>`` directory produced by SimNIBS.
342    filename : str
343        Relative path to the T1 file (default: ``segmentation/T1_bias_corrected.nii.gz``).
344
345    Raises
346    ------
347    FileNotFoundError
348    """
349    path = Path(m2m_dir) / filename
350    if not path.exists():
351        raise FileNotFoundError(f"T1 not found: {path}")
352    return path
353
354
355def get_brainmask(
356    m2m_dir: Optional[Path] = None,
357    filename: str = "label_prep/tissue_labeling_upsampled.nii.gz",
358    space: str = SPACE_NATIVE,
359    mni_mask_path: Optional[Path] = None,
360) -> Path:
361    """
362    Return the path to the brain mask.
363
364    Parameters
365    ----------
366    m2m_dir : Path or None
367        ``m2m_<subject>`` directory produced by SimNIBS. Ignored when ``space='mni'``.
368    filename : str
369        Relative path to the mask file inside ``m2m_dir`` (subject space only).
370    space : str
371        ``'native'`` (default): mask inside ``m2m_dir``.
372        ``'mni'``: MNI mask passed via ``mni_mask_path`` (read from config).
373    mni_mask_path : Path or None
374        MNI mask path, required when ``space='mni'``.
375        Must come from ``config['paths']['mni_brain_mask']``.
376
377    Raises
378    ------
379    FileNotFoundError, ValueError
380    """
381    space = normalize_space(space)
382
383    if space == SPACE_MNI:
384        if mni_mask_path is None:
385            raise ValueError(
386                "mni_mask_path is required when space='mni' (config['paths']['mni_brain_mask'])"
387            )
388        path = Path(mni_mask_path)
389    else:
390        if m2m_dir is None:
391            raise ValueError("m2m_dir is required when space='native'")
392        path = Path(m2m_dir) / filename
393    if not path.exists():
394        raise FileNotFoundError(f"Brain mask not found: {path}")
395    return path
396
397
398def get_mni_tissues(m2m_dir: Path) -> Path:
399    """
400    Return the path to the tissue segmentation in MNI space.
401
402    Produced by SimNIBS at ``toMNI/final_tissues_MNI.nii.gz``.
403    Labels: 1=WM, 2=GM, 3=CSF, 4=Bone, 5=Scalp …
404    """
405    path = Path(m2m_dir) / "toMNI" / "final_tissues_MNI.nii.gz"
406    if not path.exists():
407        raise FileNotFoundError(f"MNI tissues not found: {path}")
408    return path
409
410
411def get_roi_mask_path(
412    simnibs_output_dir: Path,
413    condition: str,
414    space: str = SPACE_MNI,
415    subject: Optional[str] = None,
416) -> Path:
417    """
418    Return the ROI mask path for a given condition.
419
420    Parameters
421    ----------
422    simnibs_output_dir : Path
423        SimNIBS output directory.
424    condition : str
425        Stimulation condition.
426    space : str
427        ``'mni'`` (default): masks in ``mni_target/``.
428        ``'native'``: subject masks in ``<subject>/subject_target/``.
429    subject : str or None
430        Subject ID, required when ``space='native'``.
431
432    Returns
433    -------
434    Path
435        ROI mask path.
436
437    Raises
438    ------
439    FileNotFoundError
440        If the requested mask does not exist.
441    """
442    space = normalize_space(space)
443
444    if space == SPACE_NATIVE:
445        if not subject:
446            raise ValueError("subject is required when space='native'")
447        mask_path = (
448            simnibs_output_dir
449            / subject
450            / "subject_target"
451            / f"{condition}_mask_{space_tag(space)}.nii.gz"
452        )
453    else:
454        mask_path = (
455            simnibs_output_dir
456            / "mni_target"
457            / f"{condition}_mask_{space_tag(space)}.nii.gz"
458        )
459
460    if not mask_path.exists():
461        raise FileNotFoundError(f"ROI mask not found ({space} space): {mask_path}")
462
463    return mask_path
464
465
466def get_preproc_dir(sim_dir: Path, mode: str, space: str = SPACE_MNI) -> Path:
467    """
468    Return the preprocessing directory for a given simulation directory.
469
470    Parameters
471    ----------
472    sim_dir : Path
473        SimNIBS simulation directory.
474    mode : str
475        ``'simulation'`` or ``'optimization'``.
476    space : str
477        ``'mni'`` (default) or ``'native'``.
478    """
479    space = normalize_space(space)
480    volumes_dir = "subject_volumes" if space == SPACE_NATIVE else "mni_volumes"
481    if mode == "optimization":
482        return sim_dir / "simulation_with_optimal_montage" / volumes_dir
483    return sim_dir / volumes_dir
484
485
486def get_preproc_paths(preproc_dir: Path, base_name: str, roi_name: str) -> dict:
487    """
488    Return the intra- and extra-ROI preprocessed file paths.
489
490    Parameters
491    ----------
492    preproc_dir : Path
493        Preprocessing output directory (e.g. mni_volumes/).
494    base_name : str
495        Base name of the e-field file (without extension).
496    roi_name : str
497        ROI name used (e.g. 'fef', 'ips_left').
498
499    Returns
500    -------
501    dict with keys:
502        ``intra_masked``, ``intra_cleaned``,
503        ``extra_masked``,  ``extra_cleaned``
504    """
505    return {
506        "intra_masked": preproc_dir / f"{base_name}_{roi_name}_masked.nii.gz",
507        "intra_cleaned": preproc_dir / f"{base_name}_{roi_name}_cleaned.nii.gz",
508        "extra_masked": preproc_dir / f"{base_name}_extra_{roi_name}_masked.nii.gz",
509        "extra_cleaned": preproc_dir / f"{base_name}_extra_{roi_name}_cleaned.nii.gz",
510    }
511
512
513def load_nifti(path: Path) -> Tuple[np.ndarray, nib.Nifti1Image]:
514    """
515    Load a NIfTI file.
516
517    Parameters
518    ----------
519    path : Path
520        Path to the NIfTI file.
521
522    Returns
523    -------
524    data : np.ndarray
525        Volume data array.
526    img : nib.Nifti1Image
527        Full NIfTI image.
528    """
529    img = nib.load(str(path))
530    data = img.get_fdata()
531    return data, img
532
533
534def load_img(
535    path_or_img: Union[str, Path, nib.spatialimages.SpatialImage],
536) -> nib.spatialimages.SpatialImage:
537    """Load a NIfTI image from a path or return the image if already loaded."""
538    if isinstance(path_or_img, (str, Path)):
539        return nib.load(str(path_or_img))
540    if isinstance(path_or_img, nib.spatialimages.SpatialImage):
541        return path_or_img
542    raise TypeError(f"Expected path or nibabel image, got {type(path_or_img)}")
543
544
545def validate_binary(data: np.ndarray, name: str = "mask") -> None:
546    """Raise ValueError if *data* contains values other than 0 and 1."""
547    unique_values = np.unique(data)
548    if not np.all(np.isin(unique_values, [0, 1])):
549        raise ValueError(
550            f"{name} must be binary (contain only 0 and 1), "
551            f"but contains values: {unique_values}"
552        )
553
554
555def check_output(path: Path, if_exists: str = "overwrite") -> bool:
556    """Return True if the file should be written, False if it should be skipped.
557
558    Parameters
559    ----------
560    path : Path
561        Target output path.
562    if_exists : str
563        ``'overwrite'`` — always write (default).
564        ``'skip'``      — silently skip if file exists.
565        ``'error'``     — raise FileExistsError if file exists.
566    """
567    path = Path(path)
568    if path.exists():
569        if if_exists == "skip":
570            logger.info(f"Skip (already exists): {path.name}")
571            return False
572        elif if_exists == "error":
573            msg = f"Output already exists (if_exists='error'): {path}"
574            logger.error(msg)
575            raise FileExistsError(msg)
576    return True
577
578
579def save_nifti(
580    img: nib.spatialimages.SpatialImage, output_path: Path, if_exists: str = "overwrite"
581) -> None:
582    """Save a NIfTI image to disk, creating parent directories as needed."""
583    output_path = Path(output_path)
584    if not check_output(output_path, if_exists):
585        return
586    output_path.parent.mkdir(parents=True, exist_ok=True)
587    img.to_filename(str(output_path))
588
589
590def load_csvs(csv_paths: Iterable[Path]) -> pd.DataFrame:
591    """Load and concatenate multiple CSV files into a single DataFrame.
592
593    Parameters
594    ----------
595    csv_paths : Iterable[Path]
596        Iterable of paths to CSV files
597
598    Returns
599    -------
600    pd.DataFrame
601        Concatenated DataFrame from all CSV files
602    """
603    frames = [pd.read_csv(p) for p in csv_paths]
604    return pd.concat(frames, ignore_index=True)
605
606
607def save_rows(rows: List[Dict], out_csv: Path, if_exists: str = "overwrite") -> None:
608    """Save a list of row dicts to a CSV file, creating parent directories as needed.
609
610    Parameters
611    ----------
612    rows :
613        List of dicts, each representing one row (e.g. feature extraction output).
614    out_csv :
615        Destination CSV path.
616    if_exists :
617        ``'overwrite'`` (default), ``'skip'``, or ``'error'``.
618    """
619    out_csv = Path(out_csv)
620    if not check_output(out_csv, if_exists):
621        return
622    out_csv.parent.mkdir(parents=True, exist_ok=True)
623    pd.DataFrame(rows).to_csv(out_csv, index=False)
624
625
626def save_dataframe(
627    df: pd.DataFrame, out_path: Path, if_exists: str = "overwrite", **to_csv_kwargs
628) -> None:
629    """Save a DataFrame to CSV, creating parent directories as needed."""
630    out_path = Path(out_path)
631    if not check_output(out_path, if_exists):
632        return
633    out_path.parent.mkdir(parents=True, exist_ok=True)
634    df.to_csv(out_path, **to_csv_kwargs)
635
636
637def save_ants_image(img: Any, out_path: Path, if_exists: str = "overwrite") -> None:
638    """Save an ANTsPy image to disk, creating parent directories as needed."""
639    import ants
640
641    out_path = Path(out_path)
642    if not check_output(out_path, if_exists):
643        return
644    out_path.parent.mkdir(parents=True, exist_ok=True)
645    ants.image_write(img, str(out_path))
646
647
648def save_figure(out_path: Path, if_exists: str = "overwrite", **savefig_kwargs) -> bool:
649    """Save the current matplotlib figure to disk.
650
651    Returns True if the figure was saved, False if skipped.
652    Closes the figure in both cases.
653    """
654    import matplotlib.pyplot as plt
655
656    out_path = Path(out_path)
657    if not check_output(out_path, if_exists):
658        logger.debug(f"  skip figure (exists): {out_path.name}")
659        plt.close()
660        return False
661    out_path.parent.mkdir(parents=True, exist_ok=True)
662    plt.savefig(out_path, **savefig_kwargs)
663    plt.close()
664    return True
logger = <simnibs_analyze._logging._PipelineLogger object>
SPACE_MNI = 'mni'
SPACE_NATIVE = 'native'
class OutputContext:
31class OutputContext:
32    """
33    Groups all parameters needed to build pipeline output filenames.
34    Instantiate in the main loop (run.py) and pass to save / get_*_path functions.
35
36    Parameters
37    ----------
38    subject : str
39        Subject identifier (e.g. '0008').
40    condition : str
41        ROI / stimulation condition name (e.g. 'fef', 'ips-left').
42        Used in preprocessed filenames and mask names.
43    mode : str
44        'simulation' or 'optimization'.
45    space : str
46        'mni' or 'native'.
47    roi_name : str
48        ROI key as defined in config.yaml → target_generation.rois.
49        Usually identical to *condition*; useful to distinguish the config key
50        from the filename.
51    base_name : str or None
52        Stem of the source e-field file (e.g. 'sub-0008_scalar_MNI_magnE').
53        Required for get_preproc_paths.  Set after find_efield_files.
54    results_dir : Path or None
55        Root results directory (config.paths.results_dir).
56    simnibs_output : Path or None
57        Root SimNIBS output directory (config.paths.simnibs_output).
58    """
59
60    def __init__(
61        self,
62        subject: str = "",
63        condition: str = "",
64        mode: str = "",
65        space: str = SPACE_MNI,
66        roi_name: str = "",
67        base_name: str = "",
68        results_dir: Optional[Path] = None,
69        simnibs_output: Optional[Path] = None,
70    ) -> None:
71        self.subject = subject
72        self.condition = condition
73        self.mode = mode
74        self.space = space
75        self.roi_name = roi_name or condition  # default: same as condition
76        self.base_name = base_name
77        self.results_dir = results_dir
78        self.simnibs_output = simnibs_output

Groups all parameters needed to build pipeline output filenames. Instantiate in the main loop (run.py) and pass to save / get_*_path functions.

Parameters

subject : str Subject identifier (e.g. '0008'). condition : str ROI / stimulation condition name (e.g. 'fef', 'ips-left'). Used in preprocessed filenames and mask names. mode : str 'simulation' or 'optimization'. space : str 'mni' or 'native'. roi_name : str ROI key as defined in config.yaml → target_generation.rois. Usually identical to condition; useful to distinguish the config key from the filename. base_name : str or None Stem of the source e-field file (e.g. 'sub-0008_scalar_MNI_magnE'). Required for get_preproc_paths. Set after find_efield_files. results_dir : Path or None Root results directory (config.paths.results_dir). simnibs_output : Path or None Root SimNIBS output directory (config.paths.simnibs_output).

OutputContext( subject: str = '', condition: str = '', mode: str = '', space: str = 'mni', roi_name: str = '', base_name: str = '', results_dir: Optional[pathlib.Path] = None, simnibs_output: Optional[pathlib.Path] = None)
60    def __init__(
61        self,
62        subject: str = "",
63        condition: str = "",
64        mode: str = "",
65        space: str = SPACE_MNI,
66        roi_name: str = "",
67        base_name: str = "",
68        results_dir: Optional[Path] = None,
69        simnibs_output: Optional[Path] = None,
70    ) -> None:
71        self.subject = subject
72        self.condition = condition
73        self.mode = mode
74        self.space = space
75        self.roi_name = roi_name or condition  # default: same as condition
76        self.base_name = base_name
77        self.results_dir = results_dir
78        self.simnibs_output = simnibs_output
subject
condition
mode
space
roi_name
base_name
results_dir
simnibs_output
def normalize_space(space: str) -> str:
81def normalize_space(space: str) -> str:
82    """Normalize and validate the working space value."""
83    normalized = str(space).lower().strip()
84    if normalized not in {SPACE_MNI, SPACE_NATIVE}:
85        raise ValueError(
86            f"Invalid 'space' parameter: {space}. Allowed values: {SPACE_MNI}, {SPACE_NATIVE}"
87        )
88    return normalized

Normalize and validate the working space value.

def space_tag(space: str) -> str:
91def space_tag(space: str) -> str:
92    """Return tagged space label used in output paths and file names."""
93    return f"space-{normalize_space(space)}"

Return tagged space label used in output paths and file names.

def get_preps_root(paths_config) -> pathlib.Path:
 96def get_preps_root(paths_config) -> Path:
 97    """Retourne la racine des segmentations charm (m2m_*)."""
 98    if paths_config.simnibs_preps is not None:
 99        return Path(paths_config.simnibs_preps)
100    return Path(paths_config.simnibs_output)

Retourne la racine des segmentations charm (m2m_*).

def get_simu_root(paths_config) -> pathlib.Path:
103def get_simu_root(paths_config) -> Path:
104    """Retourne la racine des simulations/optimisations."""
105    if paths_config.simnibs_simu is not None:
106        return Path(paths_config.simnibs_simu)
107    return Path(paths_config.simnibs_output)

Retourne la racine des simulations/optimisations.

def get_subject_paths( simnibs_output_dir: pathlib.Path, subject: str) -> Dict[str, pathlib.Path]:
110def get_subject_paths(simnibs_output_dir: Path, subject: str) -> Dict[str, Path]:
111    """Return canonical subject-level paths used across the pipeline."""
112    subject_dir = Path(simnibs_output_dir) / subject
113    return {
114        "subject_dir": subject_dir,
115        "m2m_dir": subject_dir / f"m2m_{subject}",
116        "subject_target_dir": subject_dir / "subject_target",
117    }

Return canonical subject-level paths used across the pipeline.

def get_subject_paths_from_config(paths_config, subject: str) -> Dict[str, pathlib.Path]:
120def get_subject_paths_from_config(paths_config, subject: str) -> Dict[str, Path]:
121    """
122    Return canonical subject-level paths, handling split preps/simu directories.
123
124    - ``subject_dir``        : simu root / subject  (simulations live here)
125    - ``m2m_dir``            : preps root / subject / m2m_{subject}  (charm segmentation)
126    - ``subject_target_dir`` : simu root / subject / subject_target
127    """
128    preps_root = get_preps_root(paths_config)
129    simu_root = get_simu_root(paths_config)
130    simu_subject_dir = simu_root / subject
131    preps_subject_dir = preps_root / subject
132    return {
133        "subject_dir": simu_subject_dir,
134        "m2m_dir": preps_subject_dir / f"m2m_{subject}",
135        "subject_target_dir": simu_subject_dir / "subject_target",
136    }

Return canonical subject-level paths, handling split preps/simu directories.

  • subject_dir : simu root / subject (simulations live here)
  • m2m_dir : preps root / subject / m2m_{subject} (charm segmentation)
  • subject_target_dir : simu root / subject / subject_target
def get_analysis_dir(results_dir: pathlib.Path, space: str) -> pathlib.Path:
139def get_analysis_dir(results_dir: Path, space: str) -> Path:
140    """Return the shared analysis output directory."""
141    return Path(results_dir) / "3-analysis"

Return the shared analysis output directory.

def get_features_csv_path(results_dir: pathlib.Path, space: str) -> pathlib.Path:
144def get_features_csv_path(results_dir: Path, space: str) -> Path:
145    """Return the canonical features CSV path for a given space."""
146    return get_analysis_dir(results_dir, space) / f"all_features_{space_tag(space)}.csv"

Return the canonical features CSV path for a given space.

def get_inter_subject_summary_csv_path(results_dir: pathlib.Path, space: str) -> pathlib.Path:
149def get_inter_subject_summary_csv_path(results_dir: Path, space: str) -> Path:
150    """Return the inter-subject summary CSV path for a given space."""
151    return (
152        get_analysis_dir(results_dir, space)
153        / f"inter_subject_summary_{space_tag(space)}.csv"
154    )

Return the inter-subject summary CSV path for a given space.

def get_intra_subject_diff_csv_path(results_dir: pathlib.Path, space: str, condition: str) -> pathlib.Path:
157def get_intra_subject_diff_csv_path(
158    results_dir: Path, space: str, condition: str
159) -> Path:
160    """Return the intra-subject diff CSV path for a condition and space."""
161    return (
162        get_analysis_dir(results_dir, space)
163        / f"intra_subject_diff_{condition}_{space_tag(space)}.csv"
164    )

Return the intra-subject diff CSV path for a condition and space.

def get_clusters_csv_path(results_dir: pathlib.Path, space: str) -> pathlib.Path:
167def get_clusters_csv_path(results_dir: Path, space: str) -> Path:
168    """Return the clustering CSV path for a given space."""
169    return get_analysis_dir(results_dir, space) / f"clusters_{space_tag(space)}.csv"

Return the clustering CSV path for a given space.

def load_config(config_path: pathlib.Path) -> simnibs_analyze._config.PipelineConfig:
172def load_config(config_path: Path) -> PipelineConfig:
173    """Load and validate the YAML configuration file via Pydantic models."""
174    from _config import load_and_validate
175
176    return load_and_validate(config_path)

Load and validate the YAML configuration file via Pydantic models.

def find_raw_efield( simnibs_output_dir: pathlib.Path, subject: str, roi: str, mode: str) -> Optional[pathlib.Path]:
179def find_raw_efield(
180    simnibs_output_dir: Path, subject: str, roi: str, mode: str
181) -> Optional[Path]:
182    """
183    Find the raw (unprocessed) e-field file in the SimNIBS output directory.
184
185    Parameters
186    ----------
187    simnibs_output_dir : Path
188        SimNIBS output directory.
189    subject : str
190        Subject ID.
191    roi : str
192        ROI name.
193    mode : str
194        Mode (simulation or optimization).
195
196    Returns
197    -------
198    Path or None
199        Path to the file, or None if not found.
200    """
201    subject_dir = simnibs_output_dir / subject
202
203    if not subject_dir.exists():
204        return None
205
206    if mode == "simulation":
207        base_dir = subject_dir / "simulations"
208    else:
209        base_dir = subject_dir / "optimizations"
210
211    if not base_dir.exists():
212        return None
213
214    pattern = f"{mode}_{mode}_{roi}_*"
215    matching_dirs = list(base_dir.glob(pattern))
216
217    if not matching_dirs:
218        return None
219
220    mode_dir = matching_dirs[0]
221
222    if mode == "optimization":
223        mni_volumes_dir = mode_dir / "simulation_with_optimal_montage" / "mni_volumes"
224    else:
225        mni_volumes_dir = mode_dir / "mni_volumes"
226
227    if not mni_volumes_dir.exists():
228        return None
229
230    efield_files = list(mni_volumes_dir.glob("*_scalar_MNI_magnE.nii.gz"))
231    return efield_files[0] if efield_files else None

Find the raw (unprocessed) e-field file in the SimNIBS output directory.

Parameters

simnibs_output_dir : Path SimNIBS output directory. subject : str Subject ID. roi : str ROI name. mode : str Mode (simulation or optimization).

Returns

Path or None Path to the file, or None if not found.

def find_simulation_dirs( subject_dir: pathlib.Path, condition: str, mode: str, folder_pattern: str | None = None) -> List[pathlib.Path]:
234def find_simulation_dirs(
235    subject_dir: Path, condition: str, mode: str, folder_pattern: str | None = None
236) -> List[Path]:
237    """
238    Find all simulation/optimization directories for a given condition.
239    Handles hashes in folder names.
240
241    Parameters
242    ----------
243    subject_dir : Path
244        Subject directory (e.g. 001-CC).
245    condition : str
246        Stimulation condition (e.g. fef, ips-left).
247        Used as the search fragment if *folder_pattern* is not provided.
248    mode : str
249        Mode (simulation or optimization).
250    folder_pattern : str or None
251        Glob fragment to use instead of the condition name when searching
252        SimNIBS folders.  Useful when the ROI name differs from the folder name
253        (e.g. ROI 'ips-left' but folders named '…ips_left…').
254
255    Returns
256    -------
257    List[Path]
258        List of matching directories.
259    """
260    fragment = folder_pattern if folder_pattern is not None else condition
261    pattern = f"*{mode}_{fragment}*"
262
263    if mode == "simulation":
264        base_dir = subject_dir / "simulations"
265    else:
266        base_dir = subject_dir / "optimizations"
267
268    if not base_dir.exists():
269        logger.warning(f"{mode} directory not found: {base_dir}")
270        return []
271
272    found_dirs = list(base_dir.glob(pattern))
273
274    if not found_dirs:
275        logger.warning(f"No {mode} found for pattern: {pattern} in {base_dir}")
276
277    return found_dirs

Find all simulation/optimization directories for a given condition. Handles hashes in folder names.

Parameters

subject_dir : Path Subject directory (e.g. 001-CC). condition : str Stimulation condition (e.g. fef, ips-left). Used as the search fragment if folder_pattern is not provided. mode : str Mode (simulation or optimization). folder_pattern : str or None Glob fragment to use instead of the condition name when searching SimNIBS folders. Useful when the ROI name differs from the folder name (e.g. ROI 'ips-left' but folders named '…ips_left…').

Returns

List[Path] List of matching directories.

def find_efield_files( simulation_dir: pathlib.Path, mode: str, space: str = 'mni') -> List[pathlib.Path]:
280def find_efield_files(
281    simulation_dir: Path, mode: str, space: str = SPACE_MNI
282) -> List[Path]:
283    """
284    Find e-field files in a simulation/optimization directory.
285
286    Parameters
287    ----------
288    simulation_dir : Path
289        Simulation or optimization directory.
290    mode : str
291        Mode (simulation or optimization).
292    space : str
293        ``'mni'`` (default): ``*_scalar_MNI_magnE.nii.gz`` files in ``mni_volumes/``.
294        ``'native'``: ``*_scalar_magnE.nii.gz`` files in ``subject_volumes/``.
295
296    Returns
297    -------
298    List[Path]
299        List of e-field files found.
300    """
301    space = normalize_space(space)
302
303    if space == SPACE_NATIVE:
304        if mode == "optimization":
305            volumes_dir = (
306                simulation_dir / "simulation_with_optimal_montage" / "subject_volumes"
307            )
308        else:
309            volumes_dir = simulation_dir / "subject_volumes"
310        glob_pattern = "*_scalar_magnE.nii.gz"
311    else:
312        if mode == "optimization":
313            volumes_dir = (
314                simulation_dir / "simulation_with_optimal_montage" / "mni_volumes"
315            )
316        else:
317            volumes_dir = simulation_dir / "mni_volumes"
318        glob_pattern = "*_scalar_MNI_magnE.nii.gz"
319
320    if not volumes_dir.exists():
321        logger.warning(f"{space}_volumes directory not found: {volumes_dir}")
322        return []
323
324    efield_files = list(volumes_dir.glob(glob_pattern))
325
326    if not efield_files:
327        logger.warning(f"No e-field files found in {volumes_dir}")
328
329    return efield_files

Find e-field files in a simulation/optimization directory.

Parameters

simulation_dir : Path Simulation or optimization directory. mode : str Mode (simulation or optimization). space : str 'mni' (default): *_scalar_MNI_magnE.nii.gz files in mni_volumes/. 'native': *_scalar_magnE.nii.gz files in subject_volumes/.

Returns

List[Path] List of e-field files found.

def get_t1_conform( m2m_dir: pathlib.Path, filename: str = 'segmentation/T1_bias_corrected.nii.gz') -> pathlib.Path:
332def get_t1_conform(
333    m2m_dir: Path,
334    filename: str = "segmentation/T1_bias_corrected.nii.gz",
335) -> Path:
336    """
337    Return the path to the T1 file inside ``m2m_dir``.
338
339    Parameters
340    ----------
341    m2m_dir : Path
342        ``m2m_<subject>`` directory produced by SimNIBS.
343    filename : str
344        Relative path to the T1 file (default: ``segmentation/T1_bias_corrected.nii.gz``).
345
346    Raises
347    ------
348    FileNotFoundError
349    """
350    path = Path(m2m_dir) / filename
351    if not path.exists():
352        raise FileNotFoundError(f"T1 not found: {path}")
353    return path

Return the path to the T1 file inside m2m_dir.

Parameters

m2m_dir : Path m2m_<subject> directory produced by SimNIBS. filename : str Relative path to the T1 file (default: segmentation/T1_bias_corrected.nii.gz).

Raises

FileNotFoundError

def get_brainmask( m2m_dir: Optional[pathlib.Path] = None, filename: str = 'label_prep/tissue_labeling_upsampled.nii.gz', space: str = 'native', mni_mask_path: Optional[pathlib.Path] = None) -> pathlib.Path:
356def get_brainmask(
357    m2m_dir: Optional[Path] = None,
358    filename: str = "label_prep/tissue_labeling_upsampled.nii.gz",
359    space: str = SPACE_NATIVE,
360    mni_mask_path: Optional[Path] = None,
361) -> Path:
362    """
363    Return the path to the brain mask.
364
365    Parameters
366    ----------
367    m2m_dir : Path or None
368        ``m2m_<subject>`` directory produced by SimNIBS. Ignored when ``space='mni'``.
369    filename : str
370        Relative path to the mask file inside ``m2m_dir`` (subject space only).
371    space : str
372        ``'native'`` (default): mask inside ``m2m_dir``.
373        ``'mni'``: MNI mask passed via ``mni_mask_path`` (read from config).
374    mni_mask_path : Path or None
375        MNI mask path, required when ``space='mni'``.
376        Must come from ``config['paths']['mni_brain_mask']``.
377
378    Raises
379    ------
380    FileNotFoundError, ValueError
381    """
382    space = normalize_space(space)
383
384    if space == SPACE_MNI:
385        if mni_mask_path is None:
386            raise ValueError(
387                "mni_mask_path is required when space='mni' (config['paths']['mni_brain_mask'])"
388            )
389        path = Path(mni_mask_path)
390    else:
391        if m2m_dir is None:
392            raise ValueError("m2m_dir is required when space='native'")
393        path = Path(m2m_dir) / filename
394    if not path.exists():
395        raise FileNotFoundError(f"Brain mask not found: {path}")
396    return path

Return the path to the brain mask.

Parameters

m2m_dir : Path or None m2m_<subject> directory produced by SimNIBS. Ignored when space='mni'. filename : str Relative path to the mask file inside m2m_dir (subject space only). space : str 'native' (default): mask inside m2m_dir. 'mni': MNI mask passed via mni_mask_path (read from config). mni_mask_path : Path or None MNI mask path, required when space='mni'. Must come from config['paths']['mni_brain_mask'].

Raises

FileNotFoundError, ValueError

def get_mni_tissues(m2m_dir: pathlib.Path) -> pathlib.Path:
399def get_mni_tissues(m2m_dir: Path) -> Path:
400    """
401    Return the path to the tissue segmentation in MNI space.
402
403    Produced by SimNIBS at ``toMNI/final_tissues_MNI.nii.gz``.
404    Labels: 1=WM, 2=GM, 3=CSF, 4=Bone, 5=Scalp …
405    """
406    path = Path(m2m_dir) / "toMNI" / "final_tissues_MNI.nii.gz"
407    if not path.exists():
408        raise FileNotFoundError(f"MNI tissues not found: {path}")
409    return path

Return the path to the tissue segmentation in MNI space.

Produced by SimNIBS at toMNI/final_tissues_MNI.nii.gz. Labels: 1=WM, 2=GM, 3=CSF, 4=Bone, 5=Scalp …

def get_roi_mask_path( simnibs_output_dir: pathlib.Path, condition: str, space: str = 'mni', subject: Optional[str] = None) -> pathlib.Path:
412def get_roi_mask_path(
413    simnibs_output_dir: Path,
414    condition: str,
415    space: str = SPACE_MNI,
416    subject: Optional[str] = None,
417) -> Path:
418    """
419    Return the ROI mask path for a given condition.
420
421    Parameters
422    ----------
423    simnibs_output_dir : Path
424        SimNIBS output directory.
425    condition : str
426        Stimulation condition.
427    space : str
428        ``'mni'`` (default): masks in ``mni_target/``.
429        ``'native'``: subject masks in ``<subject>/subject_target/``.
430    subject : str or None
431        Subject ID, required when ``space='native'``.
432
433    Returns
434    -------
435    Path
436        ROI mask path.
437
438    Raises
439    ------
440    FileNotFoundError
441        If the requested mask does not exist.
442    """
443    space = normalize_space(space)
444
445    if space == SPACE_NATIVE:
446        if not subject:
447            raise ValueError("subject is required when space='native'")
448        mask_path = (
449            simnibs_output_dir
450            / subject
451            / "subject_target"
452            / f"{condition}_mask_{space_tag(space)}.nii.gz"
453        )
454    else:
455        mask_path = (
456            simnibs_output_dir
457            / "mni_target"
458            / f"{condition}_mask_{space_tag(space)}.nii.gz"
459        )
460
461    if not mask_path.exists():
462        raise FileNotFoundError(f"ROI mask not found ({space} space): {mask_path}")
463
464    return mask_path

Return the ROI mask path for a given condition.

Parameters

simnibs_output_dir : Path SimNIBS output directory. condition : str Stimulation condition. space : str 'mni' (default): masks in mni_target/. 'native': subject masks in <subject>/subject_target/. subject : str or None Subject ID, required when space='native'.

Returns

Path ROI mask path.

Raises

FileNotFoundError If the requested mask does not exist.

def get_preproc_dir(sim_dir: pathlib.Path, mode: str, space: str = 'mni') -> pathlib.Path:
467def get_preproc_dir(sim_dir: Path, mode: str, space: str = SPACE_MNI) -> Path:
468    """
469    Return the preprocessing directory for a given simulation directory.
470
471    Parameters
472    ----------
473    sim_dir : Path
474        SimNIBS simulation directory.
475    mode : str
476        ``'simulation'`` or ``'optimization'``.
477    space : str
478        ``'mni'`` (default) or ``'native'``.
479    """
480    space = normalize_space(space)
481    volumes_dir = "subject_volumes" if space == SPACE_NATIVE else "mni_volumes"
482    if mode == "optimization":
483        return sim_dir / "simulation_with_optimal_montage" / volumes_dir
484    return sim_dir / volumes_dir

Return the preprocessing directory for a given simulation directory.

Parameters

sim_dir : Path SimNIBS simulation directory. mode : str 'simulation' or 'optimization'. space : str 'mni' (default) or 'native'.

def get_preproc_paths(preproc_dir: pathlib.Path, base_name: str, roi_name: str) -> dict:
487def get_preproc_paths(preproc_dir: Path, base_name: str, roi_name: str) -> dict:
488    """
489    Return the intra- and extra-ROI preprocessed file paths.
490
491    Parameters
492    ----------
493    preproc_dir : Path
494        Preprocessing output directory (e.g. mni_volumes/).
495    base_name : str
496        Base name of the e-field file (without extension).
497    roi_name : str
498        ROI name used (e.g. 'fef', 'ips_left').
499
500    Returns
501    -------
502    dict with keys:
503        ``intra_masked``, ``intra_cleaned``,
504        ``extra_masked``,  ``extra_cleaned``
505    """
506    return {
507        "intra_masked": preproc_dir / f"{base_name}_{roi_name}_masked.nii.gz",
508        "intra_cleaned": preproc_dir / f"{base_name}_{roi_name}_cleaned.nii.gz",
509        "extra_masked": preproc_dir / f"{base_name}_extra_{roi_name}_masked.nii.gz",
510        "extra_cleaned": preproc_dir / f"{base_name}_extra_{roi_name}_cleaned.nii.gz",
511    }

Return the intra- and extra-ROI preprocessed file paths.

Parameters

preproc_dir : Path Preprocessing output directory (e.g. mni_volumes/). base_name : str Base name of the e-field file (without extension). roi_name : str ROI name used (e.g. 'fef', 'ips_left').

Returns

dict with keys: intra_masked, intra_cleaned, extra_masked, extra_cleaned

def load_nifti(path: pathlib.Path) -> Tuple[numpy.ndarray, nibabel.nifti1.Nifti1Image]:
514def load_nifti(path: Path) -> Tuple[np.ndarray, nib.Nifti1Image]:
515    """
516    Load a NIfTI file.
517
518    Parameters
519    ----------
520    path : Path
521        Path to the NIfTI file.
522
523    Returns
524    -------
525    data : np.ndarray
526        Volume data array.
527    img : nib.Nifti1Image
528        Full NIfTI image.
529    """
530    img = nib.load(str(path))
531    data = img.get_fdata()
532    return data, img

Load a NIfTI file.

Parameters

path : Path Path to the NIfTI file.

Returns

data : np.ndarray Volume data array. img : nib.Nifti1Image Full NIfTI image.

def load_img( path_or_img: Union[str, pathlib.Path, nibabel.spatialimages.SpatialImage]) -> nibabel.spatialimages.SpatialImage:
535def load_img(
536    path_or_img: Union[str, Path, nib.spatialimages.SpatialImage],
537) -> nib.spatialimages.SpatialImage:
538    """Load a NIfTI image from a path or return the image if already loaded."""
539    if isinstance(path_or_img, (str, Path)):
540        return nib.load(str(path_or_img))
541    if isinstance(path_or_img, nib.spatialimages.SpatialImage):
542        return path_or_img
543    raise TypeError(f"Expected path or nibabel image, got {type(path_or_img)}")

Load a NIfTI image from a path or return the image if already loaded.

def validate_binary(data: numpy.ndarray, name: str = 'mask') -> None:
546def validate_binary(data: np.ndarray, name: str = "mask") -> None:
547    """Raise ValueError if *data* contains values other than 0 and 1."""
548    unique_values = np.unique(data)
549    if not np.all(np.isin(unique_values, [0, 1])):
550        raise ValueError(
551            f"{name} must be binary (contain only 0 and 1), "
552            f"but contains values: {unique_values}"
553        )

Raise ValueError if data contains values other than 0 and 1.

def check_output(path: pathlib.Path, if_exists: str = 'overwrite') -> bool:
556def check_output(path: Path, if_exists: str = "overwrite") -> bool:
557    """Return True if the file should be written, False if it should be skipped.
558
559    Parameters
560    ----------
561    path : Path
562        Target output path.
563    if_exists : str
564        ``'overwrite'`` — always write (default).
565        ``'skip'``      — silently skip if file exists.
566        ``'error'``     — raise FileExistsError if file exists.
567    """
568    path = Path(path)
569    if path.exists():
570        if if_exists == "skip":
571            logger.info(f"Skip (already exists): {path.name}")
572            return False
573        elif if_exists == "error":
574            msg = f"Output already exists (if_exists='error'): {path}"
575            logger.error(msg)
576            raise FileExistsError(msg)
577    return True

Return True if the file should be written, False if it should be skipped.

Parameters

path : Path Target output path. if_exists : str 'overwrite' — always write (default). 'skip' — silently skip if file exists. 'error' — raise FileExistsError if file exists.

def save_nifti( img: nibabel.spatialimages.SpatialImage, output_path: pathlib.Path, if_exists: str = 'overwrite') -> None:
580def save_nifti(
581    img: nib.spatialimages.SpatialImage, output_path: Path, if_exists: str = "overwrite"
582) -> None:
583    """Save a NIfTI image to disk, creating parent directories as needed."""
584    output_path = Path(output_path)
585    if not check_output(output_path, if_exists):
586        return
587    output_path.parent.mkdir(parents=True, exist_ok=True)
588    img.to_filename(str(output_path))

Save a NIfTI image to disk, creating parent directories as needed.

def load_csvs(csv_paths: Iterable[pathlib.Path]) -> pandas.DataFrame:
591def load_csvs(csv_paths: Iterable[Path]) -> pd.DataFrame:
592    """Load and concatenate multiple CSV files into a single DataFrame.
593
594    Parameters
595    ----------
596    csv_paths : Iterable[Path]
597        Iterable of paths to CSV files
598
599    Returns
600    -------
601    pd.DataFrame
602        Concatenated DataFrame from all CSV files
603    """
604    frames = [pd.read_csv(p) for p in csv_paths]
605    return pd.concat(frames, ignore_index=True)

Load and concatenate multiple CSV files into a single DataFrame.

Parameters

csv_paths : Iterable[Path] Iterable of paths to CSV files

Returns

pd.DataFrame Concatenated DataFrame from all CSV files

def save_rows( rows: List[Dict], out_csv: pathlib.Path, if_exists: str = 'overwrite') -> None:
608def save_rows(rows: List[Dict], out_csv: Path, if_exists: str = "overwrite") -> None:
609    """Save a list of row dicts to a CSV file, creating parent directories as needed.
610
611    Parameters
612    ----------
613    rows :
614        List of dicts, each representing one row (e.g. feature extraction output).
615    out_csv :
616        Destination CSV path.
617    if_exists :
618        ``'overwrite'`` (default), ``'skip'``, or ``'error'``.
619    """
620    out_csv = Path(out_csv)
621    if not check_output(out_csv, if_exists):
622        return
623    out_csv.parent.mkdir(parents=True, exist_ok=True)
624    pd.DataFrame(rows).to_csv(out_csv, index=False)

Save a list of row dicts to a CSV file, creating parent directories as needed.

Parameters

rows : List of dicts, each representing one row (e.g. feature extraction output). out_csv : Destination CSV path. if_exists : 'overwrite' (default), 'skip', or 'error'.

def save_dataframe( df: pandas.DataFrame, out_path: pathlib.Path, if_exists: str = 'overwrite', **to_csv_kwargs) -> None:
627def save_dataframe(
628    df: pd.DataFrame, out_path: Path, if_exists: str = "overwrite", **to_csv_kwargs
629) -> None:
630    """Save a DataFrame to CSV, creating parent directories as needed."""
631    out_path = Path(out_path)
632    if not check_output(out_path, if_exists):
633        return
634    out_path.parent.mkdir(parents=True, exist_ok=True)
635    df.to_csv(out_path, **to_csv_kwargs)

Save a DataFrame to CSV, creating parent directories as needed.

def save_ants_image(img: Any, out_path: pathlib.Path, if_exists: str = 'overwrite') -> None:
638def save_ants_image(img: Any, out_path: Path, if_exists: str = "overwrite") -> None:
639    """Save an ANTsPy image to disk, creating parent directories as needed."""
640    import ants
641
642    out_path = Path(out_path)
643    if not check_output(out_path, if_exists):
644        return
645    out_path.parent.mkdir(parents=True, exist_ok=True)
646    ants.image_write(img, str(out_path))

Save an ANTsPy image to disk, creating parent directories as needed.

def save_figure( out_path: pathlib.Path, if_exists: str = 'overwrite', **savefig_kwargs) -> bool:
649def save_figure(out_path: Path, if_exists: str = "overwrite", **savefig_kwargs) -> bool:
650    """Save the current matplotlib figure to disk.
651
652    Returns True if the figure was saved, False if skipped.
653    Closes the figure in both cases.
654    """
655    import matplotlib.pyplot as plt
656
657    out_path = Path(out_path)
658    if not check_output(out_path, if_exists):
659        logger.debug(f"  skip figure (exists): {out_path.name}")
660        plt.close()
661        return False
662    out_path.parent.mkdir(parents=True, exist_ok=True)
663    plt.savefig(out_path, **savefig_kwargs)
664    plt.close()
665    return True

Save the current matplotlib figure to disk.

Returns True if the figure was saved, False if skipped. Closes the figure in both cases.