nimare 0.4.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. benchmarks/__init__.py +0 -0
  2. benchmarks/bench_cbma.py +57 -0
  3. nimare/__init__.py +45 -0
  4. nimare/_version.py +21 -0
  5. nimare/annotate/__init__.py +21 -0
  6. nimare/annotate/cogat.py +213 -0
  7. nimare/annotate/gclda.py +924 -0
  8. nimare/annotate/lda.py +147 -0
  9. nimare/annotate/text.py +75 -0
  10. nimare/annotate/utils.py +87 -0
  11. nimare/base.py +217 -0
  12. nimare/cli.py +124 -0
  13. nimare/correct.py +462 -0
  14. nimare/dataset.py +685 -0
  15. nimare/decode/__init__.py +33 -0
  16. nimare/decode/base.py +115 -0
  17. nimare/decode/continuous.py +462 -0
  18. nimare/decode/discrete.py +753 -0
  19. nimare/decode/encode.py +110 -0
  20. nimare/decode/utils.py +44 -0
  21. nimare/diagnostics.py +510 -0
  22. nimare/estimator.py +139 -0
  23. nimare/extract/__init__.py +19 -0
  24. nimare/extract/extract.py +466 -0
  25. nimare/extract/utils.py +295 -0
  26. nimare/generate.py +331 -0
  27. nimare/io.py +667 -0
  28. nimare/meta/__init__.py +39 -0
  29. nimare/meta/cbma/__init__.py +6 -0
  30. nimare/meta/cbma/ale.py +951 -0
  31. nimare/meta/cbma/base.py +947 -0
  32. nimare/meta/cbma/mkda.py +1361 -0
  33. nimare/meta/cbmr.py +970 -0
  34. nimare/meta/ibma.py +1683 -0
  35. nimare/meta/kernel.py +501 -0
  36. nimare/meta/models.py +1199 -0
  37. nimare/meta/utils.py +494 -0
  38. nimare/nimads.py +492 -0
  39. nimare/reports/__init__.py +24 -0
  40. nimare/reports/base.py +664 -0
  41. nimare/reports/default.yml +123 -0
  42. nimare/reports/figures.py +651 -0
  43. nimare/reports/report.tpl +160 -0
  44. nimare/resources/__init__.py +1 -0
  45. nimare/resources/atlases/Harvard-Oxford-LICENSE +93 -0
  46. nimare/resources/atlases/HarvardOxford-cort-maxprob-thr25-2mm.nii.gz +0 -0
  47. nimare/resources/database_file_manifest.json +142 -0
  48. nimare/resources/english_spellings.csv +1738 -0
  49. nimare/resources/filenames.json +32 -0
  50. nimare/resources/neurosynth_laird_studies.json +58773 -0
  51. nimare/resources/neurosynth_stoplist.txt +396 -0
  52. nimare/resources/nidm_pain_dset.json +1349 -0
  53. nimare/resources/references.bib +541 -0
  54. nimare/resources/semantic_knowledge_children.txt +325 -0
  55. nimare/resources/semantic_relatedness_children.txt +249 -0
  56. nimare/resources/templates/MNI152_2x2x2_brainmask.nii.gz +0 -0
  57. nimare/resources/templates/tpl-MNI152NLin6Asym_res-01_T1w.nii.gz +0 -0
  58. nimare/resources/templates/tpl-MNI152NLin6Asym_res-01_desc-brain_mask.nii.gz +0 -0
  59. nimare/resources/templates/tpl-MNI152NLin6Asym_res-02_T1w.nii.gz +0 -0
  60. nimare/resources/templates/tpl-MNI152NLin6Asym_res-02_desc-brain_mask.nii.gz +0 -0
  61. nimare/results.py +225 -0
  62. nimare/stats.py +276 -0
  63. nimare/tests/__init__.py +1 -0
  64. nimare/tests/conftest.py +229 -0
  65. nimare/tests/data/amygdala_roi.nii.gz +0 -0
  66. nimare/tests/data/data-neurosynth_version-7_coordinates.tsv.gz +0 -0
  67. nimare/tests/data/data-neurosynth_version-7_metadata.tsv.gz +0 -0
  68. nimare/tests/data/data-neurosynth_version-7_vocab-terms_source-abstract_type-tfidf_features.npz +0 -0
  69. nimare/tests/data/data-neurosynth_version-7_vocab-terms_vocabulary.txt +100 -0
  70. nimare/tests/data/neurosynth_dset.json +2868 -0
  71. nimare/tests/data/neurosynth_laird_studies.json +58773 -0
  72. nimare/tests/data/nidm_pain_dset.json +1349 -0
  73. nimare/tests/data/nimads_annotation.json +1 -0
  74. nimare/tests/data/nimads_studyset.json +1 -0
  75. nimare/tests/data/test_baseline.txt +2 -0
  76. nimare/tests/data/test_pain_dataset.json +1278 -0
  77. nimare/tests/data/test_pain_dataset_multiple_contrasts.json +1242 -0
  78. nimare/tests/data/test_sleuth_file.txt +18 -0
  79. nimare/tests/data/test_sleuth_file2.txt +10 -0
  80. nimare/tests/data/test_sleuth_file3.txt +5 -0
  81. nimare/tests/data/test_sleuth_file4.txt +5 -0
  82. nimare/tests/data/test_sleuth_file5.txt +5 -0
  83. nimare/tests/test_annotate_cogat.py +32 -0
  84. nimare/tests/test_annotate_gclda.py +86 -0
  85. nimare/tests/test_annotate_lda.py +27 -0
  86. nimare/tests/test_dataset.py +99 -0
  87. nimare/tests/test_decode_continuous.py +132 -0
  88. nimare/tests/test_decode_discrete.py +92 -0
  89. nimare/tests/test_diagnostics.py +168 -0
  90. nimare/tests/test_estimator_performance.py +385 -0
  91. nimare/tests/test_extract.py +46 -0
  92. nimare/tests/test_generate.py +247 -0
  93. nimare/tests/test_io.py +294 -0
  94. nimare/tests/test_meta_ale.py +298 -0
  95. nimare/tests/test_meta_cbmr.py +295 -0
  96. nimare/tests/test_meta_ibma.py +240 -0
  97. nimare/tests/test_meta_kernel.py +209 -0
  98. nimare/tests/test_meta_mkda.py +234 -0
  99. nimare/tests/test_nimads.py +21 -0
  100. nimare/tests/test_reports.py +110 -0
  101. nimare/tests/test_stats.py +101 -0
  102. nimare/tests/test_transforms.py +272 -0
  103. nimare/tests/test_utils.py +200 -0
  104. nimare/tests/test_workflows.py +221 -0
  105. nimare/tests/utils.py +126 -0
  106. nimare/transforms.py +907 -0
  107. nimare/utils.py +1367 -0
  108. nimare/workflows/__init__.py +14 -0
  109. nimare/workflows/base.py +189 -0
  110. nimare/workflows/cbma.py +165 -0
  111. nimare/workflows/ibma.py +108 -0
  112. nimare/workflows/macm.py +77 -0
  113. nimare/workflows/misc.py +65 -0
  114. nimare-0.4.2.dist-info/LICENSE +21 -0
  115. nimare-0.4.2.dist-info/METADATA +124 -0
  116. nimare-0.4.2.dist-info/RECORD +119 -0
  117. nimare-0.4.2.dist-info/WHEEL +5 -0
  118. nimare-0.4.2.dist-info/entry_points.txt +2 -0
  119. nimare-0.4.2.dist-info/top_level.txt +2 -0
nimare/reports/base.py ADDED
@@ -0,0 +1,664 @@
1
+ # STATEMENT OF CHANGES: This file is derived from sources licensed under the Apache-2.0 terms,
2
+ # and this file has been changed.
3
+ # The original file this work derives from is found at:
4
+ # https://github.com/nipreps/niworkflows/blob/9905f90110879ed4123ea291f512b0a60d7ba207/niworkflows/reports/core.py
5
+ #
6
+ # [May 2023] CHANGES:
7
+ # * Replace BIDSlayout with code that uses the nimare Dataset and MetaResult class.
8
+ #
9
+ # ORIGINAL WORK'S ATTRIBUTION NOTICE:
10
+ #
11
+ # Copyright 2021 The NiPreps Developers <nipreps@gmail.com>
12
+ #
13
+ # Licensed under the Apache License, Version 2.0 (the "License");
14
+ # you may not use this file except in compliance with the License.
15
+ # You may obtain a copy of the License at
16
+ #
17
+ # http://www.apache.org/licenses/LICENSE-2.0
18
+ #
19
+ # Unless required by applicable law or agreed to in writing, software
20
+ # distributed under the License is distributed on an "AS IS" BASIS,
21
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22
+ # See the License for the specific language governing permissions and
23
+ # limitations under the License.
24
+ """Reports builder for NiMARE's MetaResult object."""
25
+ import textwrap
26
+ from glob import glob
27
+ from pathlib import Path
28
+
29
+ try:
30
+ from importlib.resources import files
31
+ except ImportError:
32
+ # Python < 3.9
33
+ from importlib_resources import files
34
+
35
+ import jinja2
36
+ import numpy as np
37
+ import pandas as pd
38
+
39
+ from nimare.meta.cbma.base import CBMAEstimator, PairwiseCBMAEstimator
40
+ from nimare.reports.figures import (
41
+ _plot_dof_map,
42
+ _plot_relcov_map,
43
+ _plot_ridgeplot,
44
+ _plot_sumstats,
45
+ _plot_true_voxels,
46
+ gen_table,
47
+ plot_clusters,
48
+ plot_coordinates,
49
+ plot_heatmap,
50
+ plot_interactive_brain,
51
+ plot_mask,
52
+ plot_static_brain,
53
+ )
54
+
55
+ PARAMETERS_DICT = {
56
+ "kernel_transformer__fwhm": "FWHM",
57
+ "kernel_transformer__sample_size": "Sample size",
58
+ "kernel_transformer__r": "Sphere radius (mm)",
59
+ "kernel_transformer__value": "Value for sphere",
60
+ "kernel_transformer__memory": "Memory",
61
+ "kernel_transformer__memory_level": "Memory level",
62
+ "kernel_transformer__sum_across_studies": "Sum Across Studies",
63
+ "memory": "Memory",
64
+ "memory_level": "Memory level",
65
+ "null_method": "Null method",
66
+ "n_iters": "Number of iterations",
67
+ "n_cores": "Number of cores",
68
+ "fwe": "Family-wise error rate (FWE) correction",
69
+ "fdr": "False discovery rate (FDR) correction",
70
+ "method": "Method",
71
+ "alpha": "Alpha",
72
+ "prior": "Prior",
73
+ "tau2": "Between-study variance",
74
+ "use_sample_size": "Use sample size for weights",
75
+ "normalize_contrast_weights": "Normalize by the number of contrasts",
76
+ "two_sided": "Two-sided test",
77
+ "beta": "Parameter estimate",
78
+ "se": "Standard error of the parameter estimate",
79
+ "varcope": "Variance of the parameter estimate",
80
+ "t": "T-statistic",
81
+ "z": "Z-statistic",
82
+ }
83
+
84
+ PNG_SNIPPET = """\
85
+ <img class="png-reportlet" src="./{0}" style="width: 100%" /></div>
86
+ <div class="elem-filename">
87
+ Get figure file: <a href="./{0}" target="_blank">{0}</a>
88
+ </div>
89
+ """
90
+
91
+ IFRAME_SNIPPET = """\
92
+ <div class="igraph-container">
93
+ <iframe class="igraph" src="./{0}"></iframe>
94
+ </div>
95
+ """
96
+
97
+ SUMMARY_TEMPLATE = """\
98
+ <ul class="elem-desc">
99
+ {meta_text}
100
+ </ul>
101
+ <details>
102
+ <summary>Experiments excluded</summary><br />
103
+ <p>{exc_ids}</p>
104
+ </details>
105
+ """
106
+
107
+ ESTIMATOR_TEMPLATE = """\
108
+ <ul class="elem-desc">
109
+ <li>Estimator: {est_name}</li>
110
+ {ker_text}
111
+ {est_params_text}
112
+ </ul>
113
+ """
114
+
115
+ CORRECTOR_TEMPLATE = """\
116
+ <ul class="elem-desc">
117
+ <li> Correction Method: {correction_method}</li>
118
+ {cor_params_text}
119
+ <li>Parameters: {ext_params_text}</li>
120
+ </ul>
121
+ """
122
+
123
+ DIAGNOSTIC_TEMPLATE = """\
124
+ <h2 class="sub-report-group">Target image: {target_image}</h2>
125
+ <ul class="elem-desc">
126
+ <li>Voxel-level threshold: {voxel_thresh}</li>
127
+ <li>Cluster size threshold: {cluster_threshold}</li>
128
+ <li>Number of cores: {n_cores}</li>
129
+ </ul>
130
+ """
131
+
132
+
133
+ def _get_cbma_summary(dset, sel_ids):
134
+ n_studies = len(dset.coordinates["study_id"].unique())
135
+
136
+ mask = dset.masker.mask_img
137
+ sel_ids = dset.get_studies_by_mask(mask)
138
+ sel_dset = dset.slice(sel_ids)
139
+
140
+ n_foci = dset.coordinates.shape[0]
141
+ n_foci_sel = sel_dset.coordinates.shape[0]
142
+ n_foci_nonbrain = n_foci - n_foci_sel
143
+
144
+ n_exps = len(dset.ids)
145
+ n_exps_sel = len(sel_ids)
146
+
147
+ cbma_text = [
148
+ f"<li>Number of studies: {n_studies:d}</li>",
149
+ f"<li>Number of experiments: {n_exps:d}</li>",
150
+ f"<li>Number of experiments included: {n_exps_sel:d}</li>",
151
+ f"<li>Number of foci: {n_foci:d} </li>",
152
+ f"<li>Number of foci outside the mask: {n_foci_nonbrain:d} </li>",
153
+ ]
154
+
155
+ return " ".join(cbma_text)
156
+
157
+
158
+ def _get_ibma_summary(dset, sel_ids):
159
+ img_df = dset.images
160
+ n_studies = len(img_df["study_id"].unique())
161
+
162
+ ignore_columns = ["id", "study_id", "contrast_id"]
163
+ map_type = [c for c in img_df if not c.endswith("__relative") and c not in ignore_columns]
164
+
165
+ n_imgs = len(dset.ids)
166
+ n_sel_ids = len(sel_ids)
167
+
168
+ ibma_text = [
169
+ f"<li>Number of studies: {n_studies:d}</li>",
170
+ f"<li>Number of images: {n_imgs:d}</li>",
171
+ f"<li>Number of images included: {n_sel_ids:d}</li>",
172
+ ]
173
+
174
+ maptype_text = ["<li>Available maps: ", "<ul>"]
175
+ maptype_text.extend(f"<li>{PARAMETERS_DICT[m]} ({m})</li>" for m in map_type)
176
+ maptype_text.extend(["</ul>", "</li>"])
177
+
178
+ ibma_text.extend(maptype_text)
179
+ return " ".join(ibma_text)
180
+
181
+
182
+ def _gen_summary(dset, sel_ids, meta_type, out_filename):
183
+ """Generate preliminary checks from dataset for the report."""
184
+ exc_ids = list(set(dset.ids) - set(sel_ids))
185
+ exc_ids_str = ", ".join(exc_ids)
186
+
187
+ meta_text = (
188
+ _get_cbma_summary(dset, sel_ids)
189
+ if meta_type == "CBMA"
190
+ else _get_ibma_summary(dset, sel_ids)
191
+ )
192
+
193
+ summary_text = SUMMARY_TEMPLATE.format(
194
+ meta_text=meta_text,
195
+ exc_ids=exc_ids_str,
196
+ )
197
+ (out_filename).write_text(summary_text, encoding="UTF-8")
198
+
199
+
200
+ def _get_kernel_summary(params_dict):
201
+ kernel_transformer = str(params_dict["kernel_transformer"])
202
+ ker_params = {k: v for k, v in params_dict.items() if k.startswith("kernel_transformer__")}
203
+ ker_params_text = ["<ul>"]
204
+ ker_params_text.extend(f"<li>{PARAMETERS_DICT[k]}: {v}</li>" for k, v in ker_params.items())
205
+ ker_params_text.append("</ul>")
206
+ ker_params_text = "".join(ker_params_text)
207
+
208
+ return f"<li>Kernel Transformer: {kernel_transformer}{ker_params_text}</li>"
209
+
210
+
211
+ def _gen_est_summary(obj, out_filename):
212
+ """Generate html with parameter use in obj (e.g., estimator)."""
213
+ params_dict = obj.get_params()
214
+
215
+ # Add kernel transformer parameters to summary if obj is a CBMAEstimator
216
+ ker_text = _get_kernel_summary(params_dict) if isinstance(obj, CBMAEstimator) else ""
217
+
218
+ est_params = {k: v for k, v in params_dict.items() if not k.startswith("kernel_transformer")}
219
+ est_params_text = [f"<li>{PARAMETERS_DICT[k]}: {v}</li>" for k, v in est_params.items()]
220
+ est_params_text = "".join(est_params_text)
221
+
222
+ est_name = obj.__class__.__name__
223
+
224
+ summary_text = ESTIMATOR_TEMPLATE.format(
225
+ est_name=est_name,
226
+ ker_text=ker_text,
227
+ est_params_text=est_params_text,
228
+ )
229
+ (out_filename).write_text(summary_text, encoding="UTF-8")
230
+
231
+
232
+ def _gen_cor_summary(obj, out_filename):
233
+ """Generate html with parameter use in obj (e.g., corrector)."""
234
+ params_dict = obj.get_params()
235
+
236
+ cor_params_text = [f"<li>{PARAMETERS_DICT[k]}: {v}</li>" for k, v in params_dict.items()]
237
+ cor_params_text = "".join(cor_params_text)
238
+
239
+ ext_params_text = ["<ul>"]
240
+ ext_params_text.extend(
241
+ f"<li>{PARAMETERS_DICT[k]}: {v}</li>" for k, v in obj.parameters.items()
242
+ )
243
+ ext_params_text.append("</ul>")
244
+ ext_params_text = "".join(ext_params_text)
245
+
246
+ summary_text = CORRECTOR_TEMPLATE.format(
247
+ correction_method=PARAMETERS_DICT[obj._correction_method],
248
+ cor_params_text=cor_params_text,
249
+ ext_params_text=ext_params_text,
250
+ )
251
+ (out_filename).write_text(summary_text, encoding="UTF-8")
252
+
253
+
254
+ def _gen_diag_summary(obj, out_filename):
255
+ """Generate html with parameter use in obj (e.g., diagnostics)."""
256
+ diag_dict = obj.get_params()
257
+
258
+ summary_text = DIAGNOSTIC_TEMPLATE.format(**diag_dict)
259
+ (out_filename).write_text(summary_text, encoding="UTF-8")
260
+
261
+
262
+ def _no_clusts_found(out_filename):
263
+ """Generate html with single text."""
264
+ null_text = '<h4 style="color:#A30000">No significant clusters found</h4>'
265
+ (out_filename).write_text(null_text, encoding="UTF-8")
266
+
267
+
268
+ def _no_maps_found(out_filename):
269
+ """Generate html with single text."""
270
+ null_text = """\
271
+ <h4 style="color:#A30000">No significant voxels were found above the threshold</h4>
272
+ """
273
+ (out_filename).write_text(null_text, encoding="UTF-8")
274
+
275
+
276
+ def _gen_fig_summary(img_key, threshold, out_filename):
277
+ summary_text = f"""\
278
+ <h2 class="sub-report-group">Corrected meta-analytic map: {img_key}</h2>
279
+ <ul class="elem-desc">
280
+ <li>Voxel-level threshold: {threshold}</li>
281
+ </ul>
282
+ """
283
+ (out_filename).write_text(summary_text, encoding="UTF-8")
284
+
285
+
286
+ def _gen_figures(results, img_key, diag_name, threshold, fig_dir):
287
+ """Generate html and png objects for the report."""
288
+ # Plot brain images if not empty
289
+ if (results.maps[img_key] > threshold).any():
290
+ img = results.get_map(img_key)
291
+ plot_interactive_brain(img, fig_dir / "corrector_figure-interactive.html", threshold)
292
+ plot_static_brain(img, fig_dir / "corrector_figure-static.png", threshold)
293
+ else:
294
+ _no_maps_found(fig_dir / "corrector_figure-non.html")
295
+
296
+ # Plot clusters table if cluster_table is not empty
297
+ cluster_table = results.tables[f"{img_key}_tab-clust"]
298
+ if cluster_table is not None and not cluster_table.empty:
299
+ gen_table(cluster_table, fig_dir / "diagnostics_tab-clust_table.html")
300
+
301
+ # Get label maps and contribution_table
302
+ contribution_tables = []
303
+ heatmap_names = []
304
+ lbl_name = "_".join(img_key.split("_")[1:])
305
+ lbl_name = f"_{lbl_name}" if lbl_name else lbl_name
306
+ for tail in ["positive", "negative"]:
307
+ lbl_key = f"label{lbl_name}_tail-{tail}"
308
+ if lbl_key in results.maps:
309
+ label_map = results.get_map(lbl_key)
310
+ plot_clusters(label_map, fig_dir / f"diagnostics_tail-{tail}_figure.png")
311
+
312
+ contribution_table_name = f"{img_key}_diag-{diag_name}_tab-counts_tail-{tail}"
313
+ if contribution_table_name in results.tables:
314
+ contribution_table = results.tables[contribution_table_name]
315
+ if contribution_table is not None and not contribution_table.empty:
316
+ contribution_table = contribution_table.set_index("id")
317
+ contribution_tables.append(contribution_table)
318
+ heatmap_names.append(
319
+ f"diagnostics_diag-{diag_name}_tab-counts_tail-{tail}_figure.html"
320
+ )
321
+
322
+ # For IBMA plot only one heatmap with both positive and negative tails
323
+ contribution_table_name = f"{img_key}_diag-{diag_name}_tab-counts"
324
+ if contribution_table_name in results.tables:
325
+ contribution_table = results.tables[contribution_table_name]
326
+ if contribution_table is not None and not contribution_table.empty:
327
+ contribution_table = contribution_table.set_index("id")
328
+ contribution_tables.append(contribution_table)
329
+ heatmap_names.append(f"diagnostics_diag-{diag_name}_tab-counts_figure.html")
330
+
331
+ # Plot heatmaps
332
+ [
333
+ plot_heatmap(contribution_table, fig_dir / heatmap_name, zmin=0)
334
+ for heatmap_name, contribution_table in zip(heatmap_names, contribution_tables)
335
+ ]
336
+
337
+ else:
338
+ _no_clusts_found(fig_dir / "diagnostics_tab-clust_table.html")
339
+
340
+
341
+ class Element(object):
342
+ """Just a basic component of a report."""
343
+
344
+ def __init__(self, name, title=None):
345
+ self.name = name
346
+ self.title = title
347
+
348
+
349
+ class Reportlet(Element):
350
+ """Reportlet holds the content of a SubReports.
351
+
352
+ A reportlet has title, description and a list of components with either an
353
+ HTML fragment or a path to an SVG file, and possibly a caption. This is a
354
+ factory class to generate Reportlets reusing the config object from a ``Report``
355
+ object.
356
+ """
357
+
358
+ def __init__(self, out_dir, config=None):
359
+ if not config:
360
+ raise RuntimeError("Reportlet must have a config object")
361
+
362
+ bids_dict = config["bids"]
363
+
364
+ # value and suffix are don't need the key, so removing from the bids conform name
365
+ keys_to_skip = ["value", "suffix"]
366
+ bids_name = "_".join("%s-%s" % i for i in bids_dict.items() if i[0] not in keys_to_skip)
367
+ bids_name = f"_{bids_name}" if bids_name else bids_name
368
+ bids_name = f"{bids_dict['value']}{bids_name}_{bids_dict['suffix']}"
369
+
370
+ self.name = config.get("name", bids_name)
371
+ self.title = config.get("title")
372
+ self.subtitle = config.get("subtitle")
373
+ self.subsubtitle = config.get("subsubtitle")
374
+ self.description = config.get("description")
375
+
376
+ files = glob(str(out_dir / "figures" / f"{self.name}.*"))
377
+
378
+ self.components = []
379
+ for file in files:
380
+ src = Path(file)
381
+ ext = "".join(src.suffixes)
382
+ desc_text = config.get("caption")
383
+ iframe = config.get("iframe", False)
384
+ dropdown = config.get("dropdown", False)
385
+
386
+ contents = None
387
+ html_anchor = src.relative_to(out_dir)
388
+ if ext == ".html":
389
+ contents = IFRAME_SNIPPET.format(html_anchor) if iframe else src.read_text()
390
+ if dropdown:
391
+ contents = (
392
+ f"<details><summary>Advanced ({self.title})</summary>{contents}</details>"
393
+ )
394
+ self.title = ""
395
+ elif ext == ".png":
396
+ contents = PNG_SNIPPET.format(html_anchor)
397
+
398
+ if contents:
399
+ self.components.append((contents, desc_text))
400
+
401
+ def is_empty(self):
402
+ """Check if the reportlet has no components."""
403
+ return len(self.components) == 0
404
+
405
+
406
+ class SubReport(Element):
407
+ """SubReports are sections within a Report."""
408
+
409
+ def __init__(self, name, isnested=False, reportlets=None, title=""):
410
+ self.name = name
411
+ self.title = title
412
+ self.reportlets = reportlets or []
413
+ self.isnested = isnested
414
+
415
+
416
+ class Report:
417
+ """The full report object.
418
+
419
+ .. versionadded:: 0.1.0
420
+
421
+ Parameters
422
+ ----------
423
+ result : :obj:`~nimare.results.MetaResult`
424
+ A MetaResult produced by a coordinate- or image-based meta-analysis.
425
+ out_dir : :obj:`str`
426
+ Output directory in which to save the report.
427
+ out_filename : :obj:`str`, optional
428
+ The name of an html file to export the report to.
429
+ Default is 'report.html'.
430
+ """
431
+
432
+ def __init__(
433
+ self,
434
+ results,
435
+ out_dir,
436
+ out_filename="report.html",
437
+ ):
438
+ self.results = results
439
+ meta_type = "CBMA" if issubclass(type(self.results.estimator), CBMAEstimator) else "IBMA"
440
+ self._is_pairwise_estimator = issubclass(
441
+ type(self.results.estimator), PairwiseCBMAEstimator
442
+ )
443
+
444
+ # Initialize structuring elements
445
+ self.sections = []
446
+ self.out_dir = Path(out_dir)
447
+ self.out_filename = out_filename
448
+
449
+ self.fig_dir = self.out_dir / "figures"
450
+ self.fig_dir.mkdir(parents=True, exist_ok=True)
451
+
452
+ if self._is_pairwise_estimator:
453
+ datasets = [self.results.estimator.dataset1, self.results.estimator.dataset2]
454
+ sel_ids = [
455
+ self.results.estimator.inputs_["id1"],
456
+ self.results.estimator.inputs_["id2"],
457
+ ]
458
+ else:
459
+ datasets = [self.results.estimator.dataset]
460
+ sel_ids = [self.results.estimator.inputs_["id"]]
461
+
462
+ for dset_i, (dataset, sel_id) in enumerate(zip(datasets, sel_ids)):
463
+ # Generate summary text
464
+ _gen_summary(
465
+ dataset,
466
+ sel_id,
467
+ meta_type,
468
+ self.fig_dir / f"preliminary_dset-{dset_i+1}_summary.html",
469
+ )
470
+
471
+ # Plot mask
472
+ plot_mask(
473
+ dataset.masker.mask_img,
474
+ self.fig_dir / f"preliminary_dset-{dset_i+1}_figure-mask.png",
475
+ )
476
+
477
+ if meta_type == "CBMA":
478
+ # Plot coordinates for CBMA estimators
479
+ plot_coordinates(
480
+ dataset.coordinates,
481
+ self.fig_dir / f"preliminary_dset-{dset_i+1}_figure-static.png",
482
+ self.fig_dir / f"preliminary_dset-{dset_i+1}_figure-interactive.html",
483
+ self.fig_dir / f"preliminary_dset-{dset_i+1}_figure-legend.png",
484
+ )
485
+ elif meta_type == "IBMA":
486
+ # Use "z_maps", for Fishers, and Stouffers; otherwise use "beta_maps".
487
+ INPUT_TYPE_LABELS = {"z_maps": "Z", "t_maps": "T", "beta_maps": "Beta"}
488
+ for key_maps, x_label in INPUT_TYPE_LABELS.items():
489
+ if key_maps in self.results.estimator.inputs_:
490
+ break
491
+ else:
492
+ key_maps, x_label = "beta_maps", "Beta"
493
+
494
+ maps_arr = self.results.estimator.inputs_[key_maps]
495
+ ids_ = self.results.estimator.inputs_["id"]
496
+
497
+ if self.results.estimator.aggressive_mask:
498
+ _plot_relcov_map(
499
+ maps_arr,
500
+ self.results.estimator.masker,
501
+ self.fig_dir / f"preliminary_dset-{dset_i+1}_figure-relcov.png",
502
+ )
503
+ else:
504
+ dof_map = self.results.get_map("dof")
505
+ _plot_dof_map(
506
+ dof_map,
507
+ self.fig_dir / f"preliminary_dset-{dset_i+1}_figure-dof.png",
508
+ )
509
+
510
+ _plot_true_voxels(
511
+ maps_arr,
512
+ ids_,
513
+ self.fig_dir / f"preliminary_dset-{dset_i+1}_figure-truevoxels.html",
514
+ )
515
+
516
+ _plot_ridgeplot(
517
+ maps_arr,
518
+ ids_,
519
+ x_label,
520
+ self.fig_dir / f"preliminary_dset-{dset_i+1}_figure-ridgeplot.html",
521
+ )
522
+
523
+ _plot_sumstats(
524
+ maps_arr,
525
+ ids_,
526
+ self.fig_dir / f"preliminary_dset-{dset_i+1}_figure-summarystats.html",
527
+ )
528
+
529
+ # Compute similarity matrix
530
+ if self.results.estimator.inputs_["corr_matrix"] is None:
531
+ if self.results.estimator.aggressive_mask:
532
+ voxel_mask = self.results.estimator.inputs_["aggressive_mask"]
533
+ corr = np.corrcoef(
534
+ self.results.estimator.inputs_[key_maps][:, voxel_mask],
535
+ rowvar=True,
536
+ )
537
+ else:
538
+ corr = np.corrcoef(
539
+ self.results.estimator.inputs_[key_maps],
540
+ rowvar=True,
541
+ )
542
+ else:
543
+ corr = self.inputs_["corr_matrix"]
544
+
545
+ similarity_table = pd.DataFrame(
546
+ index=ids_,
547
+ columns=ids_,
548
+ data=corr,
549
+ )
550
+
551
+ plot_heatmap(
552
+ similarity_table,
553
+ self.fig_dir / f"preliminary_dset-{dset_i+1}_figure-similarity.html",
554
+ symmetric=True,
555
+ cmap="RdBu_r",
556
+ zmin=-1,
557
+ zmax=1,
558
+ )
559
+
560
+ _gen_est_summary(self.results.estimator, self.fig_dir / "estimator_summary.html")
561
+ _gen_cor_summary(self.results.corrector, self.fig_dir / "corrector_summary.html")
562
+ for diagnostic in self.results.diagnostics:
563
+ img_key = diagnostic.target_image
564
+ diag_name = diagnostic.__class__.__name__
565
+ threshold = diagnostic.voxel_thresh
566
+
567
+ _gen_fig_summary(img_key, threshold, self.fig_dir / "corrector_figure-summary.html")
568
+ _gen_diag_summary(diagnostic, self.fig_dir / "diagnostics_summary.html")
569
+ _gen_figures(self.results, img_key, diag_name, threshold, self.fig_dir)
570
+
571
+ # Default template from nimare
572
+ nimare_path = files("nimare")
573
+ self.template_path = nimare_path / "reports" / "report.tpl"
574
+ self._load_config(nimare_path / "reports" / "default.yml")
575
+ assert self.template_path.exists()
576
+
577
+ def _load_config(self, config):
578
+ from yaml import safe_load as load
579
+
580
+ settings = load(config.read_text())
581
+ self.packagename = settings.get("package", None)
582
+
583
+ self.index(settings["sections"])
584
+
585
+ def index(self, config):
586
+ """Traverse the reports config definition and instantiate reportlets.
587
+
588
+ This method also places figures in their final location.
589
+ """
590
+ for subrep_cfg in config:
591
+ reportlets = [Reportlet(self.out_dir, config=cfg) for cfg in subrep_cfg["reportlets"]]
592
+
593
+ if reportlets := [r for r in reportlets if not r.is_empty()]:
594
+ sub_report = SubReport(
595
+ subrep_cfg["name"],
596
+ isnested=False,
597
+ reportlets=reportlets,
598
+ title=subrep_cfg.get("title"),
599
+ )
600
+ self.sections.append(sub_report)
601
+
602
+ def generate_report(self):
603
+ """Once the Report has been indexed, the final HTML can be generated."""
604
+ boilerplate = []
605
+ boiler_idx = 0
606
+
607
+ if hasattr(self.results, "description_"):
608
+ text = self.results.description_
609
+ references = self.results.bibtex_
610
+ text = textwrap.fill(text, 99)
611
+
612
+ boilerplate.append(
613
+ (
614
+ boiler_idx,
615
+ "LaTeX",
616
+ f"""<pre>{text}</pre>
617
+ <h3>Bibliography</h3>
618
+ <pre>{references}</pre>
619
+ """,
620
+ )
621
+ )
622
+ boiler_idx += 1
623
+
624
+ env = jinja2.Environment(
625
+ loader=jinja2.FileSystemLoader(searchpath=str(self.template_path.parent)),
626
+ trim_blocks=True,
627
+ lstrip_blocks=True,
628
+ autoescape=False,
629
+ )
630
+ report_tpl = env.get_template(self.template_path.name)
631
+ report_render = report_tpl.render(sections=self.sections, boilerplate=boilerplate)
632
+
633
+ # Write out report
634
+ self.out_dir.mkdir(parents=True, exist_ok=True)
635
+ (self.out_dir / self.out_filename).write_text(report_render, encoding="UTF-8")
636
+
637
+
638
+ def run_reports(
639
+ results,
640
+ out_dir,
641
+ ):
642
+ """Run the reports.
643
+
644
+ .. versionchanged:: 0.2.1
645
+
646
+ * Add similarity matrix to summary for image-based meta-analyses.
647
+
648
+ .. versionchanged:: 0.2.0
649
+
650
+ * Support for image-based meta-analyses.
651
+
652
+ .. versionadded:: 0.1.0
653
+
654
+ Parameters
655
+ ----------
656
+ result : :obj:`~nimare.results.MetaResult`
657
+ A MetaResult produced by a coordinate- or image-based meta-analysis.
658
+ out_dir : :obj:`str`
659
+ Output directory in which to save the report.
660
+ """
661
+ return Report(
662
+ results,
663
+ out_dir,
664
+ ).generate_report()