Diff of /dosma/defaults.py [000000] .. [030aeb]

Switch to unified view

a b/dosma/defaults.py
1
"""Default parameters.
2
3
This is the first file that is imported from this software.
4
All initialization imports should occur here.
5
6
Please change depending on your preferences.
7
"""
8
import os
9
import shutil
10
import warnings
11
import yaml
12
from typing import Any, Mapping
13
14
from dosma.utils import env
15
16
import matplotlib
17
import nested_lookup
18
19
__all__ = ["preferences"]
20
21
# Parse preferences file
22
_resources_dir = env.resources_dir()
23
_internal_preferences_template_filepath = os.path.join(_resources_dir, "templates/.preferences.yml")
24
_preferences_cmd_line_filepath = os.path.join(
25
    _resources_dir, "templates/.preferences_cmd_line_schema.yml"
26
)
27
28
__preferences_filepath__ = os.path.join(_resources_dir, "preferences.yml")
29
30
if not os.path.isfile(__preferences_filepath__):
31
    shutil.copyfile(_internal_preferences_template_filepath, __preferences_filepath__)
32
33
# Default rounding for I/O (dicom, nifti, etc) - DO NOT CHANGE
34
AFFINE_DECIMAL_PRECISION = 4
35
SCANNER_ORIGIN_DECIMAL_PRECISION = 4
36
37
DEFAULT_FONT_SIZE = 16
38
DEFAULT_TEXT_SPACING = DEFAULT_FONT_SIZE * 0.01
39
40
41
class _Preferences(object):
42
    """A pseudo-Singleton class implementation to track preferences.
43
44
    Do not instantiate this class. To modify/update preferences use the ``preferences``
45
    module variable defined below.
46
47
    However, in the case this class is instantiated, a new object will be created,
48
    but all config properties will be shared among all instances. Meaning modifying
49
    the config in one instance will impact the preferences state in another instance.
50
    """
51
52
    _template_filepath = _internal_preferences_template_filepath
53
    _preferences_filepath = __preferences_filepath__
54
    __config = {}
55
    __key_list = []
56
57
    def __init__(self):
58
        # Load config and set information if config has not been initialized.
59
        if not self.__config:
60
            with open(self._preferences_filepath, "r") as f:
61
                self.__config = yaml.safe_load(f)
62
            with open(self._template_filepath, "r") as f:
63
                template = yaml.safe_load(f)
64
65
            self.__config = self._merge_dicts(self.__config, template)
66
67
            matplotlib.rcParams.update(self.__config["visualization"]["matplotlib"]["rcParams"])
68
69
            # Store all preference keys.
70
            self.__key_list = self._unroll_keys(self.config, "")
71
72
    def _merge_dicts(self, target, base, prefix=""):
73
        """Merges dicts from target onto base."""
74
        target_keys = target.keys()
75
        base_keys = base.keys()
76
        all_keys = target_keys | base_keys
77
78
        for k in all_keys:
79
            prefix_k = f"{prefix}/{k}" if prefix else k
80
81
            if k in target_keys and k not in base_keys:
82
                # Allow target config to specify parameters not in the base config.
83
                # Note this may change in the future.
84
                pass
85
            elif k not in target_keys and k in base_keys:
86
                warnings.warn(
87
                    "Your preferences file may be outdated. "
88
                    "Run `preferences.save()` to save your updated preferences."
89
                )
90
                target[k] = base[k]
91
            else:
92
                target_v = target[k]
93
                template_v = base[k]
94
95
                if isinstance(target_v, Mapping) and isinstance(template_v, Mapping):
96
                    target[k] = self._merge_dicts(target_v, template_v, prefix=prefix_k)
97
                elif isinstance(template_v, Mapping):
98
                    raise ValueError(
99
                        f"{prefix_k}: Got target type '{type(target_v)}', "
100
                        f"but base config type is '{type(template_v)}'"
101
                    )
102
                else:
103
                    # If both are not mapping, keep the target value.
104
                    pass
105
106
        return target
107
108
    def _unroll_keys(self, subdict: dict, prefix: str) -> list:
109
        """Recursive method to unroll keys."""
110
        pref_keys = []
111
        keys = subdict.keys()
112
        for k in keys:
113
            prefix_or_key = "{}/{}".format(prefix, k)
114
            val = subdict[k]
115
            if type(val) is dict:
116
                pref_keys.extend(self._unroll_keys(val, prefix_or_key))
117
            else:
118
                pref_keys.append(prefix_or_key)
119
120
        return pref_keys
121
122
    @staticmethod
123
    def _get_prefixes(prefix: str = ""):
124
        if not prefix:
125
            return ()
126
127
        r_prefixes = []
128
        prefixes = prefix.split("/")
129
        for p in prefixes:
130
            if p == "":
131
                continue
132
            r_prefixes.append(p)
133
134
        return r_prefixes
135
136
    def get(self, key: str, prefix: str = ""):
137
        """Get preference.
138
139
        Args:
140
            key (str): Preference to peek. Can be full path preference.
141
            prefix (str, optional): Prefix defining which sub-config to search.
142
                For example, to access the key ``"visualization/rcParams"``, this parameter
143
                should be ``"visualization"``.
144
145
        Returns:
146
            The preference value.
147
        """
148
        return self.__get(self.__config, key, prefix)[0]
149
150
    def __get(self, b_dict: dict, key: str, prefix: str = ""):
151
        """Get preference.
152
153
        Args:
154
            b_dict (dict): Dictionary to search.
155
            key (str): Preference to peek. Can be full path preference.
156
            prefix (:obj:`str`, optional): Prefix defining which sub-config to search.
157
                For example, to access the key ``"visualization/rcParams"``,
158
                this parameter should be ``"visualization"``.
159
160
        Returns:
161
            The preference value, sub-dictionary to search
162
        """
163
        p_keys = self._get_prefixes(key)
164
        key = p_keys[-1]
165
        k_prefixes = list(p_keys[:-1])
166
167
        prefixes = list(self._get_prefixes(prefix))
168
        prefixes.extend(k_prefixes)
169
170
        subdict = b_dict
171
        for p in prefixes:
172
            subdict = subdict[p]
173
174
        num_keys = nested_lookup.get_occurrence_of_key(subdict, key)
175
176
        if num_keys == 0:
177
            raise KeyError("Key not '%s ' found in prefix '%s'" % (key, prefix))
178
        if num_keys > 1:
179
            raise KeyError(
180
                "Multiple keys '%s 'found in prefix '%s'. Provide a more specific prefix."
181
                % (key, prefix)
182
            )
183
184
        return nested_lookup.nested_lookup(key, subdict)[0], subdict
185
186
    def set(self, key: str, value: Any, prefix: str = ""):
187
        """Set preference.
188
189
        Args:
190
            key (str): Preference to peek. Can be full path preference.
191
            value (Any): Value to set preference.
192
            prefix (str, optional): Prefix defining which sub-config to search.
193
                For example, to access the key ``visualization/rcParams``,
194
                this parameter should be `visualization`. Defaults to ''.
195
196
        Returns:
197
            The preference value.
198
        """
199
        val, subdict = self.__get(self.__config, key, prefix)
200
201
        # type of new value has to be the same type as old value
202
        if type(value) != type(val):
203
            try:
204
                value = type(val)(value)
205
            except (ValueError, TypeError):
206
                raise TypeError("Could not convert %s to %s: %s" % (type(value), type(val), value))
207
208
        p_keys = self._get_prefixes(key)
209
        key = p_keys[-1]
210
        nested_lookup.nested_update(subdict, key, value, in_place=True)
211
212
        # if param is an rcParam, update matplotlib
213
        if key in self.config["visualization"]["matplotlib"]["rcParams"].keys():
214
            matplotlib.rcParams.update({key: value})
215
216
    def save(self):
217
        """Save preferences to file.
218
219
        Args:
220
            file_path (str, optional): File path to yml file.
221
                Defaults to local preferences file.
222
        """
223
        with open(self._preferences_filepath, "w") as f:
224
            yaml.dump(self.__config, f, default_flow_style=False)
225
226
    @property
227
    def config(self):
228
        """Get preferences configuration."""
229
        return self.__config
230
231
    # Make certain preferences easily accessible through this class.
232
    @property
233
    def segmentation_batch_size(self) -> int:
234
        """int: Batch size for segmentation models."""
235
        return self.get("/segmentation/batch.size")
236
237
    @property
238
    def visualization_use_vmax(self) -> bool:
239
        return self.get("/visualization/use.vmax")
240
241
    @property
242
    def mask_dilation_rate(self) -> float:
243
        """float: rate for mask dilation."""
244
        return self.get("/registration/mask/dilation.rate")
245
246
    @property
247
    def mask_dilation_threshold(self) -> float:
248
        """float: threshold for mask dilation."""
249
        return self.get("/registration/mask/dilation.threshold")
250
251
    @property
252
    def fitting_r2_threshold(self) -> float:
253
        """float: r2 threshold for fitting"""
254
        return self.get("/fitting/r2.threshold")
255
256
    @property
257
    def image_data_format(self):
258
        """ImageDataFormat: Format for images (e.g. png, eps, etc.)."""
259
        from dosma.core.io.format_io import ImageDataFormat
260
261
        return ImageDataFormat[self.get("/data/format")]
262
263
    @property
264
    def nipype_logging(self) -> str:
265
        """str: nipype library logging mode."""
266
        # TODO: Remove this try/except clause when schema is well defined.
267
        try:
268
            return self.get("/logging/nipype")
269
        except KeyError:
270
            return "file_stderr"
271
272
    def cmd_line_flags(self) -> dict:
273
        """Provide command line flags for changing preferences via command line.
274
275
        Not all preferences are listed here. Only those that should easily be set.
276
277
        All default values will be based on the current state of preferences,
278
        not the static state specified in yaml file.
279
280
        Returns:
281
            Preference keys with corresponding argparse kwarg dict
282
        """
283
        with open(_preferences_cmd_line_filepath) as f:
284
            cmd_line_config = yaml.safe_load(f)
285
286
        cmd_line_dict = {}
287
        for k in self.__key_list:
288
            try:
289
                argparse_kwargs, _ = self.__get(cmd_line_config, k)
290
            except KeyError:
291
                continue
292
293
            argparse_kwargs["default"] = self.get(k)
294
            argparse_kwargs["type"] = eval(argparse_kwargs["type"])
295
            cmd_line_dict[k] = argparse_kwargs
296
297
        return cmd_line_dict
298
299
    def __str__(self):
300
        return str(self.__config)
301
302
303
preferences = _Preferences()