1# -*- coding: utf-8 -*-
2"""Functions related to the documentation.
3
4docdict contains the standard documentation entries
5used across Nilearn.
6
7source: Eric Larson and MNE-python team.
8https://github.com/mne-tools/mne-python/blob/main/mne/utils/docs.py
9"""
10
11import sys
12
13
14###################################
15# Standard documentation entries
16#
17docdict = dict()
18
19NILEARN_LINKS = {"landing_page": "http://nilearn.github.io"}
20NILEARN_LINKS["input_output"] = (
21    "{}/manipulating_images/input_output.html".format(
22        NILEARN_LINKS["landing_page"])
23)
24
25# Verbose
26verbose = """
27verbose : :obj:`int`, optional
28    Verbosity level (0 means no message).
29    Default={}."""
30docdict['verbose'] = verbose.format(1)
31docdict['verbose0'] = verbose.format(0)
32
33# Resume
34docdict['resume'] = """
35resume : :obj:`bool`, optional
36    Whether to resume download of a partly-downloaded file.
37    Default=True."""
38
39# Data_dir
40docdict['data_dir'] = """
41data_dir : :obj:`pathlib.Path` or :obj:`str`, optional
42    Path where data should be downloaded. By default,
43    files are downloaded in home directory."""
44
45# URL
46docdict['url'] = """
47url : :obj:`str`, optional
48    URL of file to download.
49    Override download URL. Used for test only (or if you
50    setup a mirror of the data).
51    Default=None."""
52
53# Smoothing_fwhm
54docdict['smoothing_fwhm'] = """
55smoothing_fwhm : :obj:`float`, optional.
56    If ``smoothing_fwhm`` is not ``None``, it gives
57    the full-width at half maximum in millimeters
58    of the spatial smoothing to apply to the signal."""
59
60# Standardize
61standardize = """
62standardize : :obj:`bool`, optional.
63    If ``standardize`` is True, the data are centered and normed:
64    their mean is put to 0 and their variance is put to 1 in the
65    time dimension.
66    Default={}."""
67docdict['standardize'] = standardize.format('True')
68docdict['standardize_false'] = standardize.format('False')
69
70# detrend
71docdict['detrend'] = """
72detrend : :obj:`bool`, optional
73    Whether to detrend signals or not."""
74
75# Target_affine
76docdict['target_affine'] = """
77target_affine : :class:`numpy.ndarray`, optional.
78    If specified, the image is resampled corresponding to this new affine.
79    ``target_affine`` can be a 3x3 or a 4x4 matrix.
80    Default=None."""
81
82# Target_shape
83docdict['target_shape'] = """
84target_shape : :obj:`tuple` or :obj:`list`, optional.
85    If specified, the image will be resized to match this new shape.
86    ``len(target_shape)`` must be equal to 3.
87
88    .. note::
89        If ``target_shape`` is specified, a ``target_affine`` of shape
90        ``(4, 4)`` must also be given.
91
92    Default=None."""
93
94# Low_pass
95docdict['low_pass'] = """
96low_pass : :obj:`float` or None, optional
97    Low cutoff frequency in Hertz.
98    If None, no low-pass filtering will be performed.
99    Default=None."""
100
101# High pass
102docdict['high_pass'] = """
103high_pass : :obj:`float`, optional
104    High cutoff frequency in Hertz.
105    Default=None."""
106
107# t_r
108docdict['t_r'] = """
109t_r : :obj:`float` or None, optional
110    Repetition time, in seconds (sampling period).
111    Set to ``None`` if not provided.
112    Default=None."""
113
114# mask_img
115docdict['mask_img'] = """
116mask_img : Niimg-like object
117    Object used for masking the data."""
118
119# Memory
120docdict['memory'] = """
121memory : instance of :class:`joblib.Memory` or :obj:`str`
122    Used to cache the masking process.
123    By default, no caching is done. If a :obj:`str` is given, it is the
124    path to the caching directory."""
125
126# n_parcels
127docdict['n_parcels'] = """
128n_parcels : :obj:`int`, optional
129    Number of parcels to divide the data into.
130    Default=50."""
131
132# random_state
133docdict['random_state'] = """
134random_state : :obj:`int` or RandomState, optional
135    Pseudo-random number generator state used for random sampling."""
136
137# Memory_level
138memory_level = """
139memory_level : :obj:`int`, optional.
140    Rough estimator of the amount of memory used by caching. Higher value
141    means more memory for caching.
142    Default={}."""
143docdict['memory_level'] = memory_level.format(0)
144docdict['memory_level1'] = memory_level.format(1)
145
146# n_jobs
147n_jobs = """
148n_jobs : :obj:`int`, optional.
149    The number of CPUs to use to do the computation. -1 means 'all CPUs'.
150    Default={}."""
151docdict['n_jobs'] = n_jobs.format("1")
152docdict['n_jobs_all'] = n_jobs.format("-1")
153
154# img
155docdict['img'] = """
156img : Niimg-like object
157    See `input-output <%(input_output)s>`_.
158""" % NILEARN_LINKS
159
160# imgs
161docdict['imgs'] = """
162imgs : :obj:`list` of Niimg-like objects
163    See `input-output <%(input_output)s>`_.
164""" % NILEARN_LINKS
165
166# cut_coords
167docdict['cut_coords'] = """
168cut_coords : None, a :obj:`tuple` of :obj:`float`, or :obj:`int`, optional
169    The MNI coordinates of the point where the cut is performed.
170
171        - If ``display_mode`` is 'ortho' or 'tiled', this should
172          be a 3-tuple: ``(x, y, z)``
173        - For ``display_mode == 'x'``, 'y', or 'z', then these are
174          the coordinates of each cut in the corresponding direction.
175        - If ``None`` is given, the cuts are calculated automatically.
176        - If ``display_mode`` is 'mosaic', and the number of cuts is the same
177          for all directions, ``cut_coords`` can be specified as an integer.
178          It can also be a length 3 tuple specifying the number of cuts for
179          every direction if these are different.
180
181        .. note::
182
183            If ``display_mode`` is 'x', 'y' or 'z', ``cut_coords`` can be
184            an integer, in which case it specifies the number of
185            cuts to perform.
186
187"""
188
189# output_file
190docdict['output_file'] = """
191output_file : :obj:`str`, or None, optional
192    The name of an image file to export the plot to. Valid extensions
193    are .png, .pdf, .svg. If ``output_file`` is not None, the plot
194    is saved to a file, and the display is closed."""
195
196# extractor / extract_type
197docdict['extractor'] = """
198extractor : {'local_regions', 'connected_components'}, optional
199    This option can take two values:
200
201        - 'connected_components': each component/region in the image is
202          extracted automatically by labelling each region based upon the
203          presence of unique features in their respective regions.
204
205        - 'local_regions': each component/region is extracted based on
206          their maximum peak value to define a seed marker and then using
207          random walker segmentation algorithm on these markers for region
208          separation.
209
210    Default='local_regions'."""
211docdict['extract_type'] = docdict['extractor']
212
213# display_mode
214docdict['display_mode'] = """
215display_mode : {'ortho', 'tiled', 'mosaic','x',\
216'y', 'z', 'yx', 'xz', 'yz'}, optional
217    Choose the direction of the cuts:
218
219        - 'x': sagittal
220        - 'y': coronal
221        - 'z': axial
222        - 'ortho': three cuts are performed in orthogonal
223          directions
224        - 'tiled': three cuts are performed and arranged
225          in a 2x2 grid
226        - 'mosaic': three cuts are performed along
227          multiple rows and columns
228
229    Default='ortho'."""
230
231# figure
232docdict['figure'] = """
233figure : :obj:`int`, or :class:`matplotlib.figure.Figure`, or None,  optional
234    Matplotlib figure used or its number. If ``None`` is given, a
235    new figure is created."""
236
237# axes
238docdict['axes'] = """
239axes : :class:`matplotlib.axes.Axes`, or 4 tuple\
240of :obj:`float`: (xmin, ymin, width, height), optional
241    The axes, or the coordinates, in matplotlib figure
242    space, of the axes used to display the plot.
243    If ``None``, the complete figure is used."""
244
245# title
246docdict['title'] = """
247title : :obj:`str`, or None, optional
248    The title displayed on the figure.
249    Default=None."""
250
251# threshold
252docdict['threshold'] = """
253threshold : a number, None, or 'auto', optional
254    If ``None`` is given, the image is not thresholded.
255    If a number is given, it is used to threshold the image:
256    values below the threshold (in absolute value) are plotted
257    as transparent. If 'auto' is given, the threshold is determined
258    magically by analysis of the image.
259"""
260
261# annotate
262docdict['annotate'] = """
263annotate : :obj:`bool`, optional
264    If ``annotate`` is ``True``, positions and left/right annotation
265    are added to the plot. Default=True."""
266
267# draw_cross
268docdict['draw_cross'] = """
269draw_cross : :obj:`bool`, optional
270    If ``draw_cross`` is ``True``, a cross is drawn on the plot to indicate
271    the cut position. Default=True."""
272
273# black_bg
274docdict['black_bg'] = """
275black_bg : :obj:`bool`, or 'auto', optional
276    If ``True``, the background of the image is set to be black.
277    If you wish to save figures with a black background, you
278    will need to pass facecolor='k', edgecolor='k'
279    to :func:`matplotlib.pyplot.savefig`."""
280
281# colorbar
282docdict['colorbar'] = """
283colorbar : :obj:`bool`, optional
284    If ``True``, display a colorbar on the right of the plots."""
285
286# symmetric_cbar
287docdict['symmetric_cbar'] = """
288symmetric_cbar : :obj:`bool`, or 'auto', optional
289    Specifies whether the colorbar should range from ``-vmax`` to ``vmax``
290    or from ``vmin`` to ``vmax``. Setting to 'auto' will select the latter
291    if the range of the whole image is either positive or negative.
292
293    .. note::
294
295        The colormap will always range from ``-vmax`` to ``vmax``.
296
297"""
298
299# cbar_tick_format
300docdict['cbar_tick_format'] = """
301cbar_tick_format : :obj:`str`, optional
302    Controls how to format the tick labels of the colorbar.
303    Ex: use "%%.2g" to display using scientific notation."""
304
305# bg_img
306docdict['bg_img'] = """
307bg_img : Niimg-like object, optional
308    See `input_output <%(input_output)s>`_.
309    The background image to plot on top of.
310""" % NILEARN_LINKS
311
312# vmin
313docdict['vmin'] = """
314vmin : :obj:`float`, optional
315    Lower bound of the colormap. If ``None``, the min of the image is used.
316    Passed to :func:`matplotlib.pyplot.imshow`.
317"""
318
319# vmax
320docdict['vmax'] = """
321vmax : :obj:`float`, optional
322    Upper bound of the colormap. If ``None``, the max of the image is used.
323    Passed to :func:`matplotlib.pyplot.imshow`.
324"""
325
326# bg_vmin
327docdict['bg_vmin'] = """
328bg_vmin : :obj:`float`, optional
329    vmin for ``bg_img``."""
330
331# bg_vmax
332docdict['bg_vmax'] = """
333bg_vmin : :obj:`float`, optional
334    vmax for ``bg_img``."""
335
336# resampling_interpolation
337docdict['resampling_interpolation'] = """
338resampling_interpolation : :obj:`str`, optional
339    Interpolation to use when resampling the image to
340    the destination space. Can be:
341
342        - "continuous": use 3rd-order spline interpolation
343        - "nearest": use nearest-neighbor mapping.
344
345            .. note::
346
347                "nearest" is faster but can be noisier in some cases.
348
349"""
350
351# cmap
352docdict['cmap'] = """
353cmap : :class:`matplotlib.colors.Colormap`, or :obj:`str`, optional
354    The colormap to use. Either a string which is a name of
355    a matplotlib colormap, or a matplotlib colormap object."""
356
357# Dimming factor
358docdict['dim'] = """
359dim : :obj:`float`, or 'auto', optional
360    Dimming factor applied to background image. By default, automatic
361    heuristics are applied based upon the background image intensity.
362    Accepted float values, where a typical span is between -2 and 2
363    (-2 = increase contrast; 2 = decrease contrast), but larger values
364    can be used for a more pronounced effect. 0 means no dimming."""
365
366# avg_method
367docdict['avg_method'] = """
368avg_method : {'mean', 'median', 'min', 'max', custom function}, optional
369    How to average vertex values to derive the face value:
370
371        - ``mean``: results in smooth boundaries
372        - ``median``: results in sharp boundaries
373        - ``min`` or ``max``: for sparse matrices
374        - ``custom function``: You can also pass a custom function
375          which will be executed though :func:`numpy.apply_along_axis`.
376          Here is an example of a custom function:
377
378            .. code-block:: python
379
380                def custom_function(vertices):
381                    return vertices[0] * vertices[1] * vertices[2]
382
383"""
384
385# hemi
386docdict['hemi'] = """
387hemi : {'left', 'right'}, optional
388    Hemisphere to display. Default='left'."""
389
390# hemispheres
391docdict['hemispheres'] = """
392hemispheres : list of :obj:`str`, optional
393    Hemispheres to display. Default=['left', 'right']."""
394
395# view
396docdict['view'] = """
397view : {'lateral', 'medial', 'dorsal', 'ventral',\
398        'anterior', 'posterior'}, optional
399    View of the surface that is rendered.
400    Default='lateral'.
401"""
402
403# bg_on_data
404docdict['bg_on_data'] = """
405bg_on_data : :obj:`bool`, optional
406    If ``True``, and a ``bg_map`` is specified,
407    the ``surf_data`` data is multiplied by the background
408    image, so that e.g. sulcal depth is visible beneath
409    the ``surf_data``.
410
411        .. note::
412            This non-uniformly changes the surf_data values according
413            to e.g the sulcal depth.
414
415"""
416
417# darkness
418docdict['darkness'] = """
419darkness : :obj:`float` between 0 and 1, optional
420    Specifying the darkness of the background image:
421
422        - '1' indicates that the original values of the background are used
423        - '.5' indicates that the background values are reduced by half
424          before being applied.
425
426"""
427
428# linewidth
429docdict['linewidths'] = """
430linewidths : :obj:`float`, optional
431    Set the boundary thickness of the contours.
432    Only reflects when ``view_type=contours``."""
433
434# fsaverage options
435docdict['fsaverage_options'] = """
436
437        - 'fsaverage3': the low-resolution fsaverage3 mesh (642 nodes)
438        - 'fsaverage4': the low-resolution fsaverage4 mesh (2562 nodes)
439        - 'fsaverage5': the low-resolution fsaverage5 mesh (10242 nodes)
440        - 'fsaverage5_sphere': the low-resolution fsaverage5 spheres
441
442            .. deprecated:: 0.8.0
443                This option has been deprecated and will be removed in v0.9.0.
444                fsaverage5 sphere coordinates can now be accessed through
445                attributes sphere_{left, right} using mesh='fsaverage5'
446
447        - 'fsaverage6': the medium-resolution fsaverage6 mesh (40962 nodes)
448        - 'fsaverage7': same as 'fsaverage'
449        - 'fsaverage': the high-resolution fsaverage mesh (163842 nodes)
450
451            .. note::
452                The high-resolution fsaverage will result in more computation
453                time and memory usage
454
455"""
456
457# Classifiers
458base_url = "https://scikit-learn.org/stable/modules/generated/sklearn"
459svc = "Linear support vector classifier"
460logistic = "Logistic regression"
461rc = "Ridge classifier"
462dc = "Dummy classifier with stratified strategy"
463SKLEARN_LINKS = {
464    'svc': f"{base_url}.svm.SVC.html",
465    'logistic': f"{base_url}.linear_model.LogisticRegression.html",
466    'ridge_classifier': f"{base_url}.linear_model.RidgeClassifierCV.html",
467    'dummy_classifier': f"{base_url}.dummy.DummyClassifier.html",
468    'ridge': f"{base_url}.linear_model.RidgeCV.html",
469    'svr': f"{base_url}.svm.SVR.html",
470    'dummy_regressor': f"{base_url}.dummy.DummyRegressor.html",
471}
472
473docdict['classifier_options'] = f"""
474
475        - `svc`: `{svc} <%(svc)s>`_ with L2 penalty.
476            .. code-block:: python
477
478                svc = LinearSVC(penalty='l2',
479                                max_iter=1e4)
480
481        - `svc_l2`: `{svc} <%(svc)s>`_ with L2 penalty.
482            .. note::
483                Same as option `svc`.
484
485        - `svc_l1`: `{svc} <%(svc)s>`_ with L1 penalty.
486            .. code-block:: python
487
488                svc_l1 = LinearSVC(penalty='l1',
489                                   dual=False,
490                                   max_iter=1e4)
491
492        - `logistic`: `{logistic} <%(logistic)s>`_ with L2 penalty.
493            .. code-block:: python
494
495                logistic = LogisticRegression(penalty='l2',
496                                              solver='liblinear')
497
498        - `logistic_l1`: `{logistic} <%(logistic)s>`_ with L1 penalty.
499            .. code-block:: python
500
501                logistic_l1 = LogisticRegression(penalty='l1',
502                                                 solver='liblinear')
503
504        - `logistic_l2`: `{logistic} <%(logistic)s>`_ with L2 penalty
505            .. note::
506                Same as option `logistic`.
507
508        - `ridge_classifier`: `{rc} <%(ridge_classifier)s>`_.
509            .. code-block:: python
510
511                ridge_classifier = RidgeClassifierCV()
512
513        - `dummy_classifier`: `{dc} <%(dummy_classifier)s>`_.
514            .. code-block:: python
515
516                dummy = DummyClassifier(strategy='stratified',
517                                        random_state=0)
518
519""" % SKLEARN_LINKS
520
521docdict['regressor_options'] = """
522
523        - `ridge`: `Ridge regression <%(ridge)s>`_.
524            .. code-block:: python
525
526                ridge = RidgeCV()
527
528        - `ridge_regressor`: `Ridge regression <%(ridge)s>`_.
529            .. note::
530                Same option as `ridge`.
531
532        - `svr`: `Support vector regression <%(svr)s>`_.
533            .. code-block:: python
534
535                svr = SVR(kernel='linear',
536                          max_iter=1e4)
537
538        - `dummy_regressor`: `Dummy regressor <%(dummy_regressor)s>`_.
539            .. code-block:: python
540
541                dummy = DummyRegressor(strategy='mean')
542
543""" % SKLEARN_LINKS
544
545# mask_strategy
546docdict["mask_strategy"] = """
547mask_strategy : {'background', 'epi', 'whole-brain-template',\
548'gm-template', 'wm-template'}, optional
549    The strategy used to compute the mask:
550
551        - 'background': Use this option if your images present
552          a clear homogeneous background.
553        - 'epi': Use this option if your images are raw EPI images
554        - 'whole-brain-template': This will extract the whole-brain
555          part of your data by resampling the MNI152 brain mask for
556          your data's field of view.
557
558            .. note::
559                This option is equivalent to the previous 'template' option
560                which is now deprecated.
561
562        - 'gm-template': This will extract the gray matter part of your
563          data by resampling the corresponding MNI152 template for your
564          data's field of view.
565
566            .. versionadded:: 0.8.1
567
568        - 'wm-template': This will extract the white matter part of your
569          data by resampling the corresponding MNI152 template for your
570          data's field of view.
571
572            .. versionadded:: 0.8.1
573
574"""
575
576docdict_indented = {}
577
578
579def _indentcount_lines(lines):
580    """Minimum indent for all lines in line list.
581
582    >>> lines = [' one', '  two', '   three']
583    >>> _indentcount_lines(lines)
584    1
585    >>> lines = []
586    >>> _indentcount_lines(lines)
587    0
588    >>> lines = [' one']
589    >>> _indentcount_lines(lines)
590    1
591    >>> _indentcount_lines(['    '])
592    0
593
594    """
595    indentno = sys.maxsize
596    for line in lines:
597        stripped = line.lstrip()
598        if stripped:
599            indentno = min(indentno, len(line) - len(stripped))
600    if indentno == sys.maxsize:
601        return 0
602    return indentno
603
604
605def fill_doc(f):
606    """Fill a docstring with docdict entries.
607
608    Parameters
609    ----------
610    f : callable
611        The function to fill the docstring of. Will be modified in place.
612
613    Returns
614    -------
615    f : callable
616        The function, potentially with an updated ``__doc__``.
617
618    """
619    docstring = f.__doc__
620    if not docstring:
621        return f
622    lines = docstring.splitlines()
623    # Find the minimum indent of the main docstring, after first line
624    if len(lines) < 2:
625        icount = 0
626    else:
627        icount = _indentcount_lines(lines[1:])
628    # Insert this indent to dictionary docstrings
629    try:
630        indented = docdict_indented[icount]
631    except KeyError:
632        indent = ' ' * icount
633        docdict_indented[icount] = indented = {}
634        for name, dstr in docdict.items():
635            lines = dstr.splitlines()
636            try:
637                newlines = [lines[0]]
638                for line in lines[1:]:
639                    newlines.append(indent + line)
640                indented[name] = '\n'.join(newlines)
641            except IndexError:
642                indented[name] = dstr
643    try:
644        f.__doc__ = docstring % indented
645    except (TypeError, ValueError, KeyError) as exp:
646        funcname = f.__name__
647        funcname = docstring.split('\n')[0] if funcname is None else funcname
648        raise RuntimeError('Error documenting %s:\n%s'
649                           % (funcname, str(exp)))
650    return f
651