11import os
2- from nltools .utils import get_resource_path
3-
4- __all__ = ["MNI_Template" , "resolve_mni_path" ]
5-
6-
7- class MNI_Template_Factory (dict ):
8- """Class to build the default MNI_Template dictionary. This should never be used
9- directly, instead just `from nltools.prefs import MNI_Template` and update that
10- object's attributes to change MNI templates."""
11-
12- def __init__ (
13- self ,
14- resolution = "2mm" ,
15- mask_type = "with_ventricles" ,
16- mask = os .path .join (get_resource_path (), "MNI152_T1_2mm_brain_mask.nii.gz" ),
17- plot = os .path .join (get_resource_path (), "MNI152_T1_2mm.nii.gz" ),
18- brain = os .path .join (get_resource_path (), "MNI152_T1_2mm_brain.nii.gz" ),
19- ):
20- self ._resolution = resolution
21- self ._mask_type = mask_type
22- self ._mask = mask
23- self ._plot = plot
24- self ._brain = brain
25-
26- self .update (
27- {
28- "resolution" : self .resolution ,
29- "mask_type" : self .mask_type ,
30- "mask" : self .mask ,
31- "plot" : self .plot ,
32- "brain" : self .brain ,
33- }
34- )
35-
36- @property
37- def resolution (self ):
38- return self ._resolution
39-
40- @resolution .setter
41- def resolution (self , resolution ):
42- if isinstance (resolution , (int , float )):
43- resolution = f"{ int (resolution )} mm"
44- if resolution not in ["2mm" , "3mm" ]:
45- raise NotImplementedError (
46- "Only 2mm and 3mm resolutions are currently supported"
47- )
48- self ._resolution = resolution
49- self .update ({"resolution" : self ._resolution })
50-
51- @property
52- def mask_type (self ):
53- return self ._mask_type
54-
55- @mask_type .setter
56- def mask_type (self , mask_type ):
57- if mask_type not in ["with_ventricles" , "no_ventricles" ]:
58- raise NotImplementedError (
59- "Only 'with_ventricles' and 'no_ventricles' mask_types are currently supported"
60- )
61- self ._mask_type = mask_type
62- self .update ({"mask_type" : self ._mask_type })
63-
64- @property
65- def mask (self ):
66- return self ._mask
67-
68- @mask .setter
69- def mask (self , mask ):
70- self ._mask = mask
71- self .update ({"mask" : self ._mask })
72-
73- @property
74- def plot (self ):
75- return self ._plot
76-
77- @plot .setter
78- def plot (self , plot ):
79- self ._plot = plot
80- self .update ({"plot" : self ._plot })
81-
82- @property
83- def brain (self ):
84- return self ._brain
85-
86- @brain .setter
87- def brain (self , brain ):
88- self ._brain = brain
89- self .update ({"brain" : self ._brain })
90-
91-
92- # NOTE: We export this from the module and expect users to interact with it instead of
93- # the class constructor above
94- MNI_Template = MNI_Template_Factory ()
95-
96-
97- def resolve_mni_path (MNI_Template ):
98- """Helper function to resolve MNI path based on MNI_Template prefs setting."""
99-
100- res = MNI_Template ["resolution" ]
101- m = MNI_Template ["mask_type" ]
102- if not isinstance (res , str ):
103- raise ValueError ("resolution must be provided as a string!" )
104- if not isinstance (m , str ):
105- raise ValueError ("mask_type must be provided as a string!" )
106-
107- if res == "3mm" :
108- if m == "with_ventricles" :
109- MNI_Template ["mask" ] = os .path .join (
110- get_resource_path (), "MNI152_T1_3mm_brain_mask.nii.gz"
111- )
112- elif m == "no_ventricles" :
113- MNI_Template ["mask" ] = os .path .join (
114- get_resource_path (), "MNI152_T1_3mm_brain_mask_no_ventricles.nii.gz"
115- )
116- else :
117- raise ValueError (
118- "Available mask_types are 'with_ventricles' or 'no_ventricles'"
119- )
120-
121- MNI_Template ["plot" ] = os .path .join (get_resource_path (), "MNI152_T1_3mm.nii.gz" )
122-
123- MNI_Template ["brain" ] = os .path .join (
124- get_resource_path (), "MNI152_T1_3mm_brain.nii.gz"
2+ from dataclasses import dataclass , field
3+ from typing import Literal
4+ from os .path import dirname , join
5+
6+ __all__ = ["MNI_Template" ]
7+
8+
9+ @dataclass
10+ class MNI_Template_Factory :
11+ """Global MNI template configuration.
12+
13+ This class manages the global MNI template settings used throughout nltools.
14+ Users should interact with the exported MNI_Template instance rather than
15+ creating new instances.
16+
17+ Parameters
18+ ----------
19+ template : {'default', 'nilearn', 'fmriprep'}
20+ Template variant to use. Each template represents a different MNI space:
21+ - 'default': Original MNI152 6th generation templates
22+ - 'nilearn': Nilearn's MNI152 templates
23+ - 'fmriprep': fMRIPrep's MNI152NLin2009cAsym templates
24+ resolution : {1, 2, 3}
25+ Resolution in mm. Not all resolutions are available for all templates:
26+ - 'default': 2mm, 3mm
27+ - 'nilearn': 1mm, 2mm, 3mm
28+ - 'fmriprep': 1mm, 2mm
29+
30+ Attributes
31+ ----------
32+ mask : str
33+ Path to the brain mask file
34+ brain : str
35+ Path to the brain-extracted image
36+ plot : str
37+ Path to the full T1 image for plotting
38+
39+ Examples
40+ --------
41+ >>> from nltools.prefs import MNI_Template
42+ >>> MNI_Template.template = 'fmriprep'
43+ >>> MNI_Template.resolution = 1
44+ >>> print(MNI_Template.mask)
45+ """
46+
47+ template : Literal ["default" , "nilearn" , "fmriprep" ] = "default"
48+ resolution : Literal [1 , 2 , 3 ] = 2
49+
50+ # Auto-populated paths
51+ mask : str = field (init = False )
52+ brain : str = field (init = False )
53+ plot : str = field (init = False )
54+
55+ # Define supported combinations
56+ _supported_combinations = {
57+ "default" : [2 , 3 ],
58+ "nilearn" : [1 , 2 , 3 ],
59+ "fmriprep" : [1 , 2 ]
60+ }
61+
62+ def __post_init__ (self ):
63+ """Initialize paths after dataclass creation."""
64+ self ._validate_and_resolve ()
65+
66+ def __setattr__ (self , name , value ):
67+ """Custom setter to re-resolve paths when attributes change."""
68+ # Use object.__setattr__ to avoid recursion
69+ object .__setattr__ (self , name , value )
70+ # Only resolve paths if we're setting template or resolution
71+ # and the object has been fully initialized
72+ if name in ["template" , "resolution" ] and hasattr (self , "_validate_and_resolve" ):
73+ self ._validate_and_resolve ()
74+
75+ def __repr__ (self ) -> str :
76+ return (
77+ f"MNI_Template(template='{ self .template } ', resolution={ self .resolution } mm)\n "
78+ f" mask: { os .path .basename (self .mask )} \n "
79+ f" brain: { os .path .basename (self .brain )} \n "
80+ f" plot: { os .path .basename (self .plot )} "
12581 )
126-
127- elif res == "2mm" :
128- if m == "with_ventricles" :
129- MNI_Template ["mask" ] = os .path .join (
130- get_resource_path (), "MNI152_T1_2mm_brain_mask.nii.gz"
131- )
132- elif m == "no_ventricles" :
133- MNI_Template ["mask" ] = os .path .join (
134- get_resource_path (), "MNI152_T1_2mm_brain_mask_no_ventricles.nii.gz"
135- )
136- else :
82+
83+ def _validate_and_resolve (self ):
84+ """Validate inputs and resolve file paths."""
85+ # Validate resolution is supported for this template
86+ if self .resolution not in self ._supported_combinations .get (self .template , []):
13787 raise ValueError (
138- "Available mask_types are 'with_ventricles' or 'no_ventricles'"
88+ f"Resolution { self .resolution } mm is not supported for template '{ self .template } '. "
89+ f"Supported resolutions: { self ._supported_combinations [self .template ]} "
13990 )
140-
141- MNI_Template ["plot" ] = os .path .join (get_resource_path (), "MNI152_T1_2mm.nii.gz" )
142-
143- MNI_Template ["brain" ] = os .path .join (
144- get_resource_path (), "MNI152_T1_2mm_brain.nii.gz"
145- )
146- else :
147- raise ValueError ("Available templates are '2mm' or '3mm'" )
148- return MNI_Template
91+
92+ # Build paths based on template and resolution
93+ base_path = join (dirname (__file__ ), "resources" , "niftis" , self .template )
94+ res_str = f"{ self .resolution } mm"
95+
96+ # Set paths following the naming convention
97+ self .mask = join (base_path , f"MNI152_{ res_str } _mask.nii.gz" )
98+ self .brain = join (base_path , f"MNI152_{ res_str } _brain.nii.gz" )
99+ self .plot = join (base_path , f"MNI152_{ res_str } _T1.nii.gz" )
100+
101+ # Verify files exist
102+ for attr , path in [("mask" , self .mask ), ("brain" , self .brain ), ("plot" , self .plot )]:
103+ if not os .path .exists (path ):
104+ raise FileNotFoundError (
105+ f"Template file not found: { path } \n "
106+ f"This suggests an incomplete installation or missing template files."
107+ )
108+
109+
110+ # NOTE: We export this from the module and expect users to interact with it instead of
111+ # the class constructor above
112+ MNI_Template = MNI_Template_Factory ()
0 commit comments