diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 4a079703..0013e92f 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -9,7 +9,7 @@ version: 2 build: os: ubuntu-20.04 tools: - python: "3.9" + python: "3.8" # You can also specify other tool versions: # nodejs: "16" # rust: "1.55" diff --git a/README.md b/README.md index 7b18d703..f786d7e0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ OHBA Software Library (OSL) =========================== +Documentation: https://osl.readthedocs.io/en/latest/. + Install from Source Code ------------------------ The recommended installation depends on your operating system. OSL can be installed from source using: @@ -14,11 +16,10 @@ pip install -e . where the environment file `.yml` can be: - `linux.yml` for a generic linux machine. -- `m1_mac.yml` if you are using a modern Mac computer. - `hbaws.yml` if you are using an OHBA workstation at Oxford. - `bmrc.yml` if you are using the BMRC at Oxford. -Note, all of the above environments come with Jupyter Notebook installed. The `hbaws.yml` and `m1_mac.yml` environments also comes with Spyder installed. +Note, all of the above environments come with Jupyter Notebook installed. The `hbaws.yml` environment also comes with Spyder installed. Deleting osl ------------ diff --git a/doc/source/api.rst b/doc/source/api.rst index aa80526f..a157ce66 100644 --- a/doc/source/api.rst +++ b/doc/source/api.rst @@ -1,5 +1,5 @@ API -================ +=== This page is the reference for the functions in OSL. @@ -162,9 +162,11 @@ Utility functions for running an OSL Preprocessing pipeline. GLM -****** +*** + GLM Base ++++++++ + Modality specific wrappers for glmtools. .. currentmodule:: osl.glm.glm_base @@ -193,7 +195,7 @@ Modality specific wrappers for glmtools. SensorClusterPerm.get_sig_clusters GLM Epochs -++++++++++++++++++++++++ +++++++++++ .. currentmodule:: osl.glm.glm_epochs @@ -227,7 +229,7 @@ GLM Epochs GLM Spectrum -++++++++++++++++++++++++ +++++++++++++ GLM-Spectrum classes and functions designed to work with GLM-Spectra computed from MNE format sensorspace data @@ -284,11 +286,13 @@ GLM-Spectrum classes and functions designed to work with GLM-Spectra computed fr RHINO -******************** +***** + Tools for Coregistration and forward modeling Coregistration ++++++++++++++ + Deep-level RHINO functions used by wrappers .. currentmodule:: osl.source_recon.rhino.coreg @@ -309,6 +313,7 @@ Deep-level RHINO functions used by wrappers Forward modeling ++++++++++++++++ + Deep-level RHINO functions used by wrappers .. currentmodule:: osl.source_recon.rhino.forward_model @@ -324,7 +329,8 @@ Deep-level RHINO functions used by wrappers FSL-utils -+++++++++++ ++++++++++ + Deep-level RHINO functions used by wrappers .. currentmodule:: osl.source_recon.rhino.fsl_utils @@ -342,7 +348,8 @@ Deep-level RHINO functions used by wrappers Polhemus -+++++++++++ +++++++++ + Deep-level RHINO functions used by wrappers .. currentmodule:: osl.source_recon.rhino.polhemus @@ -358,7 +365,8 @@ Deep-level RHINO functions used by wrappers Surfaces -+++++++++++ +++++++++ + Deep-level RHINO functions used by wrappers .. currentmodule:: osl.source_recon.rhino.surfaces @@ -378,7 +386,8 @@ Deep-level RHINO functions used by wrappers Utils -+++++++++++ ++++++ + Deep-level RHINO functions used by wrappers .. currentmodule:: osl.source_recon.rhino.utils @@ -448,7 +457,7 @@ Deep-level RHINO functions used by wrappers Source Reconstruction -********************** +********************* Pipeline Functions ++++++++++++++++++ @@ -467,6 +476,7 @@ Primary user-level functions for running OSL coregistration and source_recon fun Wrappers ++++++++ + Primary wrapper functions to use in a source_recon configuration .. currentmodule:: osl.source_recon.wrappers @@ -499,6 +509,7 @@ Primary wrapper functions to use in a source_recon configuration Beamforming +++++++++++ + Second-level beamforming functions used by wrappers .. currentmodule:: osl.source_recon.beamforming @@ -533,9 +544,9 @@ Second-level beamforming functions used by wrappers _prepare_beamformer_input - Sign-flipping +++++++++++++ + Second-level sign-flipping functions used by wrappers .. currentmodule:: osl.source_recon.sign_flipping @@ -566,6 +577,7 @@ Second-level sign-flipping functions used by wrappers Utils +++++ + Utility functions for running an OSL Source Recon pipeline. .. currentmodule:: osl.source_recon.batch @@ -580,6 +592,7 @@ Utility functions for running an OSL Source Recon pipeline. Parcellation ************ + Second-level Parcellation functions used by wrappers .. currentmodule:: osl.source_recon.parcellation.parcellation @@ -620,6 +633,7 @@ Second-level Parcellation functions used by wrappers Nifti Utils ++++++++++++ + Second-level Parcellation functions used by wrappers .. currentmodule:: osl.source_recon.parcellation.nii @@ -638,9 +652,11 @@ Second-level Parcellation functions used by wrappers Report ****** + Sensor level ++++++++++++ -.. currentmodule:: osl.report.raw_report + +.. currentmodule:: osl.report.preproc_report .. autosummary:: :toctree: stubs @@ -656,6 +672,7 @@ Sensor level Source level ++++++++++++ + .. currentmodule:: osl.report.src_report .. autosummary:: @@ -685,7 +702,7 @@ File handling Logger -++++++++++++ +++++++ .. currentmodule:: osl.utils.logger @@ -735,4 +752,4 @@ OPM Utilities convert_notts - correct_mri \ No newline at end of file + correct_mri diff --git a/doc/source/conf.py b/doc/source/conf.py index 9938c57f..cddbe488 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -6,23 +6,27 @@ # -- Path setup -------------------------------------------------------------- +import os +import sys +import inspect + +__location__ = os.path.join( + os.getcwd(), os.path.dirname(inspect.getfile(inspect.currentframe())) +) + # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -# -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) - +sys.path.insert(0, os.path.join(__location__, "../..")) # -- Project information ----------------------------------------------------- -project = 'osl' -copyright = '2023, OMG' -author = 'OMG' +project = 'OSL' +copyright = '2023, OHBA Analysis Group' +author = 'OHBA Analysis Group' # The full version, including alpha/beta/rc tags -release = '0.0.1dev' +release = '0.5.1' # -- General configuration --------------------------------------------------- diff --git a/doc/source/install.rst b/doc/source/install.rst index 88f4ad6a..8e937f32 100644 --- a/doc/source/install.rst +++ b/doc/source/install.rst @@ -1,12 +1,28 @@ Installation ============ -First, make sure you have conda (or miniconda) installed: https://docs.conda.io/en/latest. Then oslpy can be installed from source using the following: +We recommend installing OSL within a virtual environment. You can do this with `Anaconda `_ (or `miniconda `_). -:: - - git clone https://github.com/OHBA-analysis/oslpy.git - cd oslpy +Linux +----- + +1. Install FSL using the instructions `here `_. + +2. Install OSL either via pip using:: + + pip install osl + +or from source (in editable mode) using:: + + git clone https://github.com/OHBA-analysis/osl.git + cd osl conda env create -f envs/linux.yml conda activate osl pip install -e . + +Windows +------- + +If you're using a Windows machine, we recommend you install FSL within a `Ubuntu `_ (linux) subsystem following the instructions `here `_. + +Then install OSL using the instructions above. diff --git a/envs/m1_mac.yml b/envs/m1-mac.yml similarity index 100% rename from envs/m1_mac.yml rename to envs/m1-mac.yml diff --git a/envs/m2_mac.yml b/envs/m2-mac.yml similarity index 100% rename from envs/m2_mac.yml rename to envs/m2-mac.yml diff --git a/envs/windows.yml b/envs/windows.yml new file mode 100644 index 00000000..b534ebe8 --- /dev/null +++ b/envs/windows.yml @@ -0,0 +1,46 @@ +name: osl +channels: +- conda-forge +dependencies: +- python=3.8.16 +- pip=23.0.1 +- vtk=9.1.0=*osmesa* +- pyvista=0.38.5 +- pip: + - jupyter==1.0.0 + - ipympl==0.9.3 + - ipywidgets==8.0.5 + - ipyevents==2.0.1 + - ipyvtklink==0.2.2 + - jupyter-client==8.1.0 + - numpy==1.23.5 + - scipy==1.10.1 + - matplotlib==3.7.1 + - mne==1.3.1 + - scikit-learn==1.2.2 + - fslpy==3.11.3 + - sails==1.4.0 + - tabulate==0.9.0 + - pyyaml==6.0 + - neurokit2==0.2.3 + - jinja2==3.1.2 + - glmtools==0.2.1 + - numba==0.56.4 + - nilearn==0.10.2 + - dask==2023.3.2 + - distributed==2023.3.2 + - parse==1.19.0 + - opencv-python==4.7.0.72 + - h5io==0.1.7 + - mat73==0.60 + - nibabel==5.0.1 + - pandas==1.5.3 + - panel==1.2.3 + - pqdm==0.2.0 + - seaborn==0.12.2 + - tensorflow==2.9.1 + - tensorflow_probability==0.17.0 + - tqdm==4.65.0 + - osfclient==0.0.5 + - osl==0.5.1 + - osl-dynamics==1.2.11 diff --git a/examples/extract_rhino_files.py b/examples/extract_rhino_files.py deleted file mode 100644 index 4abf3bc3..00000000 --- a/examples/extract_rhino_files.py +++ /dev/null @@ -1,57 +0,0 @@ -"""Example script for extracting previously computed surfaces and coregistration. - -Sometimes newer version of OSL aren't backwards compatible. This maybe an issue -if you want to use files created with an older version of OSL. This often happens -in the coregistration step, where you want to use previously computed surfaces -and coregistration. - -In this script, we extract previously computed surfaces and the coregistation and -create a new directory which will work for the latest version of OSL. - -We give example code for doing this in serial and in parallel using the batch -processing - you don't need to do both. -""" - -# --------- -# In serial - -from osl.source_recon import setup_fsl -from osl.source_recon.rhino.utils import extract_rhino_files - -setup_fsl("/opt/ohba/fsl/6.0.5") - -old_dir = "path/to/old/src/dir" -new_dir = "src" - -extract_rhino_files(old_dir, new_dir) - -# ----------- -# In parallel - -from glob import glob -from dask.distributed import Client - -from osl import source_recon, utils - -if __name__ == "__main__": - utils.logger.set_up(level="INFO") - source_recon.setup_fsl("/opt/ohba/fsl/6.0.5") - - old_dir = "path/to/old/src/dir" - new_dir = "src" - - subjects = [path.split("/")[-1] for path in sorted(glob(f"{old_dir}/sub-*"))] - - config = f""" - source_recon: - - extract_rhino_files: {{old_src_dir: {old_dir}}} - """ - - client = Client(n_workers=16, threads_per_worker=1) - - source_recon.run_src_batch( - config, - src_dir=new_dir, - subjects=subjects, - dask_client=True, - ) diff --git a/examples/fix_smri_files.py b/examples/fix_smri_files.py new file mode 100644 index 00000000..59cb2c0e --- /dev/null +++ b/examples/fix_smri_files.py @@ -0,0 +1,29 @@ +"""Fix sform code of structurals. + +This script uses FSL to set the sform code of any structural +whose sform code is not 1 or 4 to make sure it is compatible +with OSL. + +Warning: this script will permanently change the SMRI file. +""" + +import nibabel as nib + +from osl import source_recon + +source_recon.setup_fsl("/path/to/fsl") + +# Paths to files to fix +files = [ + "smri/sub-001.nii.gz", + "smri/sub-002.nii.gz", +] + +for file in files: + smri = nib.load(file) + sformcode = smri.header.get_sform(coded=True)[-1] + if sformcode not in [1, 4]: + cmd = f"fslorient -setsformcode 1 {file}" + source_recon.rhino.utils.system_call(cmd) + +print("Done”) diff --git a/examples/oxford/ssp_pipeline/README.md b/examples/oxford/ssp_pipeline/README.md index dce5e0cc..e113c40b 100644 --- a/examples/oxford/ssp_pipeline/README.md +++ b/examples/oxford/ssp_pipeline/README.md @@ -1,5 +1,5 @@ -Manual ICA Pipeline -------------------- +SSP Pipeline +------------ The scripts in this directory contain the recommended settings for preprocessing continuous data collected using the new scanner (Neo) at OHBA, Oxford. It doesn't matter if the data was recorded during a task or at rest. diff --git a/osl/preprocessing/batch.py b/osl/preprocessing/batch.py index e51d1498..7ff94075 100644 --- a/osl/preprocessing/batch.py +++ b/osl/preprocessing/batch.py @@ -22,6 +22,7 @@ from functools import partial, wraps from time import localtime, strftime from datetime import datetime +import inspect import mne import numpy as np @@ -299,7 +300,7 @@ def get_config_from_fif(inst): return config -def append_preproc_info(dataset, config): +def append_preproc_info(dataset, config, extra_funcs=None): """Add to the config of already preprocessed data to ``inst.info['description']``. Parameters @@ -325,6 +326,12 @@ def append_preproc_info(dataset, config): + f"VERSION: {__version__}\n" + f"%% config start %% \n{config} \n%% config end %%" ) + + if extra_funcs is not None: + preproc_info += "\n\nCUSTOM FUNCTIONS USED:\n" + for func in extra_funcs: + preproc_info += f"%% extra_funcs start %% \n{inspect.getsource(func)}\n%% extra_funcs end %%" + dataset["raw"].info["description"] = ( dataset["raw"].info["description"] + preproc_info ) @@ -745,7 +752,7 @@ def run_proc_chain( dataset = func(dataset, userargs) # Add preprocessing info to dataset dict - dataset = append_preproc_info(dataset, config) + dataset = append_preproc_info(dataset, config, extra_funcs) fif_outname = None if outdir is not None: diff --git a/osl/report/preproc_report.py b/osl/report/preproc_report.py index 6926017e..4fc5e4f1 100644 --- a/osl/report/preproc_report.py +++ b/osl/report/preproc_report.py @@ -15,6 +15,7 @@ import tempfile import pickle import pathlib +import re import numpy as np import matplotlib.pyplot as plt @@ -192,6 +193,8 @@ def gen_html_data(raw, outdir, ica=None, preproc_fif_filename=None): # Generate plots for the report data["plt_config"] = plot_flowchart(raw, savebase) + data["txt_extra_funcs"] = save_extra_funcs(raw, savebase.replace('.png', '.txt')) + data["plt_rawdata"] = plot_rawdata(raw, savebase) data['plt_temporalsumsq'] = plot_channel_time_series(raw, savebase, exclude_bads=False) data['plt_temporalsumsq_exclude_bads'] = plot_channel_time_series(raw, savebase, exclude_bads=True) data['plt_badchans'] = plot_sensors(raw, savebase) @@ -317,6 +320,7 @@ def gen_html_summary(reportdir): os.makedirs(f"{reportdir}/summary", exist_ok=True) data["plt_config"] = subject_data[0]["plt_config"] + data["txt_extra_funcs"] = subject_data[0]["txt_extra_funcs"] data["plt_summary_bad_segs"] = plot_summary_bad_segs(subject_data, reportdir) data["plt_summary_bad_chans"] = plot_summary_bad_chans(subject_data, reportdir) @@ -419,6 +423,72 @@ def plot_flowchart(raw, savebase=None): fpath = None return fpath + +def save_extra_funcs(raw, savebase=None): + """ Saves extra functions from the raw.info['description'] to a text file. + + Parameters + ---------- + raw : :py:class:`mne.io.Raw ` + MNE Raw object. + savebase : str + Base string for saving figures. + + Returns + ------- + fpath : str + Path to saved text file. + + """ + extra_funcs = re.findall( + "%% extra_funcs start %%(.*?)%% extra_funcs end %%", + raw.info["description"], + flags=re.DOTALL, + ) + + if savebase is not None: + fpath = savebase.format(f"extra_funcs") + with(open(fpath, 'w')) as file: + [print(func, file=file) for func in extra_funcs] + return fpath + else: + return None + + + +def plot_rawdata(raw, savebase): + """Plots raw data. + + Parameters + ---------- + raw : :py:class:`mne.io.Raw ` + MNE Raw object. + savebase : str + Base string for saving figures. + + Returns + ------- + fpath : str + Path to saved figure. + + """ + + fig = raw.pick(['meg', 'eeg']).plot(n_channels=np.inf, duration=raw.times[-1]) + + if savebase is not None: + figname = savebase.format('rawdata') + fig.savefig(figname, dpi=150, transparent=True) + plt.close(fig) + + # Return the filename + savebase = pathlib.Path(savebase) + filebase = savebase.parent.name + "/" + savebase.name + fpath = filebase.format('rawdata') + else: + fpath = None + return fpath + + def plot_channel_time_series(raw, savebase=None, exclude_bads=False): """Plots sum-square time courses. @@ -744,13 +814,12 @@ def plot_spectra(raw, savebase=None): continue # Plot spectra - raw.plot_psd( - show=False, - picks=chan_inds, - ax=ax[row], + raw.compute_psd( + picks=chan_inds, n_fft=int(raw.info['sfreq']*2), - verbose=0, - ) + verbose=0).plot( + axes=ax[row], + show=False) ax[row].set_title(name, fontsize=12) @@ -773,15 +842,14 @@ def plot_spectra(raw, savebase=None): continue # Plot zoomed in spectra - raw.plot_psd( - show=False, - picks=chan_inds, - ax=ax[row], - fmin=1, - fmax=48, - n_fft=int(raw.info['sfreq']*2), - verbose=0, - ) + raw.compute_psd( + picks=chan_inds, + fmin=1, + fmax=48, + n_fft=int(raw.info['sfreq']*2), + verbose=0).plot( + axes=ax[row], + show=False) ax[row].set_title(name, fontsize=12) diff --git a/osl/report/src_report.py b/osl/report/src_report.py index 03e5ca68..cf43a1c4 100644 --- a/osl/report/src_report.py +++ b/osl/report/src_report.py @@ -3,6 +3,7 @@ """ # Authors: Chetan Gohil +# Mats van Es import os import os.path as op @@ -13,12 +14,13 @@ import numpy as np import matplotlib.pyplot as plt from tabulate import tabulate +import inspect from . import preproc_report from ..source_recon import parcellation -def gen_html_data(config, src_dir, subject, reportdir, logger=None): +def gen_html_data(config, src_dir, subject, reportdir, logger=None, extra_funcs=None): """Generate data for HTML report. Parameters @@ -33,6 +35,8 @@ def gen_html_data(config, src_dir, subject, reportdir, logger=None): Report directory. logger : logging.getLogger Logger. + extra_funcs : list + List of extra functions to run """ src_dir = Path(src_dir) reportdir = Path(reportdir) @@ -63,6 +67,8 @@ def gen_html_data(config, src_dir, subject, reportdir, logger=None): data = {} data["config"] = config + data['extra_funcs'] = save_extra_funcs(extra_funcs, reportdir / subject) + data["fif_id"] = subject data["filename"] = subject @@ -116,6 +122,31 @@ def gen_html_data(config, src_dir, subject, reportdir, logger=None): pickle.dump(data, open(f"{reportdir}/{subject}/data.pkl", "wb")) +def save_extra_funcs(extra_funcs, reportdir): + """ Saves extra functions to a text file. + + Parameters + ---------- + extra_funcs : list + List of extra functions to save. + reportdir : str + Subject report directory. + + Returns + ------- + fpath : str + Path to saved text file. + """ + + if reportdir is not None: + fpath = reportdir / 'extra_funcs.txt' + with(open(fpath, 'w')) as file: + [print(f"{inspect.getsource(func)}\n\n", file=file) for func in extra_funcs] + return fpath + else: + return None + + def gen_html_page(reportdir): """Generate an HTML page from a report directory. @@ -215,6 +246,7 @@ def gen_html_summary(reportdir): data = {} data["total"] = total data["config"] = subject_data[0]["config"] + data["extra_funcs"] = subject_data[0]["extra_funcs"] data["coregister"] = subject_data[0]["coregister"] data["beamform"] = subject_data[0]["beamform"] data["beamform_and_parcellate"] = subject_data[0]["beamform_and_parcellate"] diff --git a/osl/report/templates/raw_subject_panel.html b/osl/report/templates/raw_subject_panel.html index 901923e1..62e43380 100644 --- a/osl/report/templates/raw_subject_panel.html +++ b/osl/report/templates/raw_subject_panel.html @@ -49,6 +49,10 @@

{{ data.fif_id }}   ({{ data.num }} of {{ data.total }})