diff --git a/CHANGELOG.md b/CHANGELOG.md index 394ad23..8f3b9c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ noted in the changelog (i.e new functions or parameters, changes in parameter de improvements/enhancements. Fixes and modifications will be backwards compatible. - *.postN* : Consists of only metadata-related changes, such as updates to type hints or doc strings/documentation. +## [0.19.3] - 2024-12-08 +### 🚀 New/Added +- Method chaining for several methods in the `CAP` and `TimeseriesExtractor` class. + ## [0.19.2] - 2024-12-06 ### 🐛 Fixes - Add type hints to properties. diff --git a/README.md b/README.md index 6257b98..b1dbaef 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # neurocaps [![Latest Version](https://img.shields.io/pypi/v/neurocaps.svg)](https://pypi.python.org/pypi/neurocaps/) [![Python Versions](https://img.shields.io/pypi/pyversions/neurocaps.svg)](https://pypi.python.org/pypi/neurocaps/) -[![DOI](https://img.shields.io/badge/DOI-10.5281%2Fzenodo.11642615-teal)](https://doi.org/10.5281/zenodo.14286989) +[![DOI](https://img.shields.io/badge/DOI-10.5281%2Fzenodo.11642615-teal)](https://doi.org/10.5281/zenodo.14322286) [![Github Repository](https://img.shields.io/badge/Source%20Code-neurocaps-purple)](https://github.com/donishadsmith/neurocaps) [![Test Status](https://github.com/donishadsmith/neurocaps/actions/workflows/testing.yaml/badge.svg)](https://github.com/donishadsmith/neurocaps/actions/workflows/testing.yaml) [![codecov](https://codecov.io/github/donishadsmith/neurocaps/graph/badge.svg?token=WS2V7I16WF)](https://codecov.io/github/donishadsmith/neurocaps) @@ -230,7 +230,9 @@ extractor.get_bold( n_cores=None, pipeline_name="fmriprep", # Can specify if multiple pipelines exists in derivatives directory verbose=True, -) +).timeseries_to_pickle( + "neurocaps_demo/derivatives", "timeseries.pkl" +) # Method chaining supported in >= 0.19.3 and available for several methods in `CAP` and `TimeseriesExtractor` classes ``` **Output:** ``` @@ -249,6 +251,7 @@ extractor.get_bold( # Get CAPs cap_analysis = CAP(parcel_approach=extractor.parcel_approach) +# Pkl files can also be used as input for `subject_timeseries` cap_analysis.get_caps(subject_timeseries=extractor.subject_timeseries, n_clusters=2, standardize=True) # `sharey` only applicable to outer product plots diff --git a/docs/introduction.rst b/docs/introduction.rst index 7e8ddf8..6d4ab10 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -9,7 +9,7 @@ :alt: Python Versions .. image:: https://img.shields.io/badge/DOI-10.5281%2Fzenodo.11642615-teal - :target: https://doi.org/10.5281/zenodo.14286989 + :target: https://doi.org/10.5281/zenodo.14322286 :alt: DOI .. image:: https://img.shields.io/badge/Source%20Code-neurocaps-purple @@ -86,7 +86,7 @@ Citing ------ :: - Smith, D. (2024). neurocaps. Zenodo. https://doi.org/10.5281/zenodo.14286989 + Smith, D. (2024). neurocaps. Zenodo. https://doi.org/10.5281/zenodo.14322286 Usage ----- diff --git a/neurocaps/__init__.py b/neurocaps/__init__.py index bad1b9a..a77458e 100644 --- a/neurocaps/__init__.py +++ b/neurocaps/__init__.py @@ -3,4 +3,4 @@ __all__=["analysis", "extraction"] # Version in single place -__version__ = "0.19.2" +__version__ = "0.19.3" diff --git a/neurocaps/analysis/cap.py b/neurocaps/analysis/cap.py index 2e7751f..7a9f247 100644 --- a/neurocaps/analysis/cap.py +++ b/neurocaps/analysis/cap.py @@ -508,6 +508,13 @@ def get_caps(self, - step: :obj:`int`, default=None An integer value that controls the progression of the x-axis in plots for the specified ``cluster_selection_method``. When set, only integer values will be displayed on the x-axis. + + Returns + ------- + self + + + .. versionadded:: 0.19.3 """ self._n_cores = n_cores # Ensure all unique values if n_clusters is a list @@ -559,6 +566,8 @@ def get_caps(self, # Create states dict self._create_caps_dict() + return self + @staticmethod def _process_subject_timeseries(subject_timeseries): if isinstance(subject_timeseries, str) and subject_timeseries.endswith(".pkl"): @@ -1430,6 +1439,13 @@ def caps2plot(self, - vmax: :obj:`float` or :obj:`None`, default=None The maximum value to display in colormap. + Returns + ------- + self + + + .. versionadded:: 0.19.3 + Note ---- **Parcellation Approach**: the "nodes" and "regions" sub-keys are required in ``parcel_approach`` for this @@ -1524,6 +1540,8 @@ def caps2plot(self, if plot_option == "outer_product": self._generate_outer_product_plots(**input_keys, subplots=subplots) elif plot_option == "heatmap": self._generate_heatmap_plots(**input_keys) + return self + def _create_regions(self, parcellation_name): # Internal function to create an attribute called `region_caps`. Purpose is to average the values of all nodes # in a corresponding region to create region heatmaps or outer product plots @@ -1947,7 +1965,7 @@ def caps2corr(self, Returns ------- - `dict[str, pd.DataFrame]` + dict[str, pd.DataFrame] An instance of a pandas DataFrame for each group. Note @@ -2063,6 +2081,13 @@ def caps2niftis(self, .. versionchanged:: 0.18.0 "remove_subcortical" key changed to "remove_labels" and default of "k" changed 1 to 3 + Returns + ------- + self + + + .. versionadded:: 0.19.3 + Note ---- **Assumption**: This function assumes that the background label for the parcellation is zero. Additionaly, @@ -2103,6 +2128,8 @@ def caps2niftis(self, nib.save(stat_map, os.path.join(output_dir, filename)) + return self + @staticmethod def _validate_knn_dict(knn_dict): valid_atlases = ["Schaefer", "AAL"] @@ -2282,6 +2309,13 @@ def caps2surf(self, - bbox_inches: :obj:`str` or :obj:`None`, default="tight" Alters size of the whitespace in the saved image. + Returns + ------- + self + + + .. versionadded:: 0.19.3 + Note ---- **Parcellation Approach**: ``parcel_approach`` must have the "maps" sub-key containing the path to th @@ -2409,6 +2443,8 @@ def caps2surf(self, try: plt.show(fig) if show_figs else plt.close(fig) except: plt.show() if show_figs else plt.close() + return self + def caps2radar(self, output_dir: Optional[os.PathLike]=None, suffix_title: Optional[str]=None, @@ -2588,6 +2624,13 @@ def caps2radar(self, - engine: {"kaleido", "orca"}, default="kaleido" Engine used for saving plots. + Returns + ------- + self + + + .. versionadded:: 0.19.3 + Note ----- **Saving Plots**: By default, this function uses "kaleido" (which is also a dependency in this package) @@ -2741,6 +2784,8 @@ def caps2radar(self, filename = filename.replace(".png", ".html") fig.write_html(os.path.join(output_dir, filename)) + return self + def _update_radar_dict(self, group, parcellation_name, radar_dict): for cap in self._caps[group]: cap_vector = self._caps[group][cap] diff --git a/neurocaps/analysis/change_dtype.py b/neurocaps/analysis/change_dtype.py index 2c36c0a..77187d9 100644 --- a/neurocaps/analysis/change_dtype.py +++ b/neurocaps/analysis/change_dtype.py @@ -63,7 +63,8 @@ def change_dtype(subject_timeseries_list: Union[list[dict[str, dict[str, NDArray Returns ------- - `dict[str, dict[str, dict[str, np.ndarray]]]` + dict[str, dict[str, dict[str, np.ndarray]]] + A nested dictionary containing the converted subject timeseries. Warning ------- diff --git a/neurocaps/analysis/merge.py b/neurocaps/analysis/merge.py index 73d8dd1..e7b53e7 100644 --- a/neurocaps/analysis/merge.py +++ b/neurocaps/analysis/merge.py @@ -22,14 +22,14 @@ def merge_dicts(subject_timeseries_list: Union[list[dict[str, dict[str, NDArray[ For example, if three dictionaries are provided contain subject 1 with: - dict 1: run-1 (resting-state) - - dict 2: run-1 and run-2 (Stroop) - - dict 3: run-3 (N-back) + - dict 2: run-1 and run-2 (stroop) + - dict 3: run-3 (n-back) Then subject 1 in the final merged dictionary will contain: - - run-1: concatenated timeseries from dict 1 and dict 2 (resting-state + Stroop) - - run-2: timeseries from dict 2 (Stroop) - - run-3: timeseries from dict 3 (N-back) + - run-1: concatenated timeseries from dict 1 and dict 2 (resting-state + stroop) + - run-2: timeseries from dict 2 (stroop) + - run-3: timeseries from dict 3 (n-back) This function supports workflows for identifying similar CAPs across tasks or sessions. Specifically, using the merged dictionary as input for ``CAP.get_caps`` and the filtered input dictionaries, containing only subjects @@ -87,7 +87,8 @@ def merge_dicts(subject_timeseries_list: Union[list[dict[str, dict[str, NDArray[ Returns ------- - `dict[str, dict[str, dict[str, np.ndarray]]]` + dict[str, dict[str, dict[str, np.ndarray]]] + A nested dictionary containing the merged subject timeseries and reduced subject timeseries if specified. References ---------- diff --git a/neurocaps/analysis/standardize.py b/neurocaps/analysis/standardize.py index 668fa71..1d801c8 100644 --- a/neurocaps/analysis/standardize.py +++ b/neurocaps/analysis/standardize.py @@ -61,7 +61,8 @@ def standardize(subject_timeseries_list: Union[list[dict[str, dict[str, NDArray[ Returns ------- - `dict[str, dict[str, dict[str, np.ndarray]]]` + dict[str, dict[str, dict[str, np.ndarray]]] + A nested dictionary containing the standardized subject timeseries. """ assert isinstance(subject_timeseries_list, list) and len(subject_timeseries_list) > 0, \ "`subject_timeseries_list` must be a list greater than length 0." diff --git a/neurocaps/analysis/transition_matrix.py b/neurocaps/analysis/transition_matrix.py index 0ca0530..dd27b3d 100644 --- a/neurocaps/analysis/transition_matrix.py +++ b/neurocaps/analysis/transition_matrix.py @@ -110,7 +110,7 @@ def transition_matrix(trans_dict: dict[str, pd.DataFrame], Returns ------- - `dict[str, pd.DataFrame]` + dict[str, pd.DataFrame] An instance of a pandas DataFrame for each group. Note diff --git a/neurocaps/extraction/timeseriesextractor.py b/neurocaps/extraction/timeseriesextractor.py index 39484bc..190b333 100644 --- a/neurocaps/extraction/timeseriesextractor.py +++ b/neurocaps/extraction/timeseriesextractor.py @@ -561,6 +561,13 @@ def get_bold(self, flush: :obj:`bool`, default=False If True, flushes the logged subject-specific information produced during the timeseries extraction process. + Returns + ------- + self + + + .. versionadded:: 0.19.3 + Note ---- **Subject Timeseries Dictionary**: This method stores the extracted timeseries of all subjects @@ -665,6 +672,8 @@ def get_bold(self, # Aggregate new timeseries if isinstance(subject_timeseries, dict): self._subject_timeseries.update(subject_timeseries) + return self + @staticmethod @lru_cache(maxsize=4) def _call_layout(bids_dir, pipeline_name): @@ -912,6 +921,13 @@ def timeseries_to_pickle(self, output_dir: Union[str, os.PathLike], filename: Op Name of the file with or without the "pkl" extension. .. versionchanged:: 0.19.0 ``file_name`` to ``filename`` + + Returns + ------- + self + + + .. versionadded:: 0.19.3 """ if not self.subject_timeseries: self._raise_error("Cannot save pickle file") @@ -923,6 +939,8 @@ def timeseries_to_pickle(self, output_dir: Union[str, os.PathLike], filename: Op with open(os.path.join(output_dir, save_filename), "wb") as f: dump(self._subject_timeseries, f) + return self + def visualize_bold(self, subj_id: Union[int, str], run: Union[int, str], @@ -977,6 +995,13 @@ def visualize_bold(self, - bbox_inches: :obj:`str` or :obj:`None`, default="tight" Alters size of the whitespace in the saved image. + Returns + ------- + self + + + .. versionadded:: 0.19.3 + Note ---- **Parcellation Approach**: the "nodes" and "regions" sub-keys are required in ``parcel_approach``. @@ -1032,6 +1057,8 @@ def visualize_bold(self, plt.show() if show_figs else plt.close() + return self + def _get_roi_indices(self, roi_indx, parcellation_name): if isinstance(roi_indx, int): plot_indxs = roi_indx diff --git a/tests/test_CAP.py b/tests/test_CAP.py index 7f1701d..e5daedc 100644 --- a/tests/test_CAP.py +++ b/tests/test_CAP.py @@ -947,3 +947,10 @@ def test_check_raise_error(): for i in ["caps", "parcel_approach", "kmeans"]: with pytest.raises(AttributeError, match=re.escape(error_msg[i])): CAP._raise_error(i) + +def test_chain_CAP(): + a = {"show_figs" : False} + cap_analysis = CAP(parcel_approach) + cap_analysis.get_caps( + subject_timeseries=extractor.subject_timeseries, + n_clusters=2).caps2plot(**a) \ No newline at end of file diff --git a/tests/test_TimeseriesExtractor.py b/tests/test_TimeseriesExtractor.py index 7f4da76..1cf91db 100644 --- a/tests/test_TimeseriesExtractor.py +++ b/tests/test_TimeseriesExtractor.py @@ -1230,3 +1230,11 @@ def test_check_raise_error(): with pytest.raises(AttributeError, match=re.escape(msg)): TimeseriesExtractor._raise_error("Cannot do x") + +def test_chain_TimeseriesExtractor(): + a = {"show_figs": False} + extractor = TimeseriesExtractor() + extractor.get_bold( + bids_dir=bids_dir, + task="rest", + run_subjects=["01"]).timeseries_to_pickle(tmp_dir.name).visualize_bold("01", 0, 0, **a) \ No newline at end of file