Commit 75fc2ef4 authored by William Clarke's avatar William Clarke
Browse files

Merge branch 'master' into 'master'

Master

See merge request !14
parents 69b1442e 086ab398
Pipeline #6972 passed with stages
in 5 minutes and 8 seconds
###########################################################################
# This file defines the build process for fsl-mrs, as hosted at:
#
# https://git.fmrib.ox.ac.uk/fsl/fsl_mrs
#
# The build pipeline currently comprises the following stages:
#
# 1. style: Check coding style - allowed to fail
#
# 2. test: Unit tests
#
# 3. doc: Building user documentation which appears at:
# https://open.win.ox.ac.uk/pages/fsl/fsl_mrs/
#
# 4. build
# & deploy: Build in three stages fsl-ci-pre, fsl-ci-build,
# sl-ci-deploy
#
# A custom docker image is used for the test job - images are
# available at:
#
# https://hub.docker.com/u/wtclarke/
#
# Stage run conditions:
# Style is run in all cases, but allowed to fail.
# Test is run in all cases.
# Doc is only run on master branches.
# Build stages are run according to the rules associated
# with https://git.fmrib.ox.ac.uk/fsl/fsl-ci-rules
#
###########################################################################
include: include:
- project: fsl/fsl-ci-rules - project: fsl/fsl-ci-rules
file: .gitlab-ci.yml file: .gitlab-ci.yml
stages: stages:
- Static Analysis - style
- Test - test
- doc - doc
- fsl-ci-pre - fsl-ci-pre
- fsl-ci-build - fsl-ci-build
- fsl-ci-deploy - fsl-ci-deploy
####################################
# These anchors are used to restrict
# when and where jobs are executed.
####################################
.only_upstream: &only_upstream
only:
- branches@fsl/fsl_mrs
.only_master: &only_master
only:
- master
.only_releases: &only_releases
only:
- tags@fsl/fsl_mrs
.except_releases: &except_releases
except:
- tags
# ############
# # 1. style
# ############
flake8: flake8:
image: python:3.7-slim-buster image: python:3.7-slim-buster
stage: Static Analysis stage: style
before_script: before_script:
- python --version - python --version
- pip install flake8 - pip install flake8
script: script:
- flake8 --max-line-length=120 fsl_mrs - flake8 fsl_mrs
allow_failure: true allow_failure: true
############
# 2. test
############
pytest: pytest:
image: wtclarke/fsl_mrs_tests:1.0 image: wtclarke/fsl_mrs_tests:1.0
stage: Test stage: test
variables: variables:
GIT_SUBMODULE_STRATEGY: normal GIT_SUBMODULE_STRATEGY: normal
before_script: before_script:
...@@ -36,14 +100,18 @@ pytest: ...@@ -36,14 +100,18 @@ pytest:
script: script:
- pytest fsl_mrs/tests - pytest fsl_mrs/tests
############
# 3. doc
############
pages: pages:
<<: *only_master
image: python:3.7 image: python:3.7
stage: doc stage: doc
script: script:
- pip install -U sphinx sphinx_rtd_theme - pip install -U sphinx sphinx_rtd_theme
- pip install .
- sphinx-build -b html ./docs/user_docs public - sphinx-build -b html ./docs/user_docs public
artifacts: artifacts:
paths: paths:
- public - public
rules:
- if: '$CI_COMMIT_BRANCH == "master"'
This document contains the FSL-MRS release history in reverse chronological order. This document contains the FSL-MRS release history in reverse chronological order.
1.0.6 (Tuesday 12th January 2021)
---------------------------------
- Internal changes to core MRS class.
- New plotting functions added, utility functions for plotting added to MRS class.
- fsl_mrs/aux folder renamed for Windows compatibility.
- Moved online documentation to open.win.ox.ac.uk/pages/fsl/fsl_mrs/.
- Fixed small bugs in preprocessing display.
- Synthetic spectra now use fitting model directly.
- Bug fixes in the fsl_Mrs commandline interface. Thanks to Alex Craig-Craven.
- WIP: Dynamic fitting model and dynamic experiment simulation.
- spec2nii requirement pinned to 0.2.11 during NIFTI-MRS development.
1.0.5 (Friday 9th October 2020) 1.0.5 (Friday 9th October 2020)
------------------------------- -------------------------------
- Extended documentation of hardcoded constants, including MCMC priors. - Extended documentation of hardcoded constants, including MCMC priors.
......
...@@ -9,7 +9,7 @@ FSL-MRS is a collection of python modules and wrapper scripts for pre-processing ...@@ -9,7 +9,7 @@ FSL-MRS is a collection of python modules and wrapper scripts for pre-processing
### Installation ### Installation
#### Conda package #### Conda package
The primary installation method is via _conda_. After installing conda and creating or activating a suitable [enviroment](https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html) you can install FSL-MRS from the FSL conda channel. The primary installation method is via _conda_. After installing conda and creating or activating a suitable [environment](https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html) you can install FSL-MRS from the FSL conda channel.
conda install -c conda-forge \ conda install -c conda-forge \
-c https://fsl.fmrib.ox.ac.uk/fsldownloads/fslconda/channel/ \ -c https://fsl.fmrib.ox.ac.uk/fsldownloads/fslconda/channel/ \
...@@ -32,7 +32,7 @@ or ...@@ -32,7 +32,7 @@ or
pip install spec2nii pip install spec2nii
After installation see the [quick start guide](https://users.fmrib.ox.ac.uk/~saad/fsl_mrs/html/quick_start.html). After installation see the [quick start guide](https://open.win.ox.ac.uk/pages/fsl/fsl_mrs/quick_start.html).
--- ---
...@@ -59,7 +59,7 @@ After installation see the [quick start guide](https://users.fmrib.ox.ac.uk/~saa ...@@ -59,7 +59,7 @@ After installation see the [quick start guide](https://users.fmrib.ox.ac.uk/~saa
### Documentation ### Documentation
Documentation can be found online [here](https://users.fmrib.ox.ac.uk/~saad/fsl_mrs/html/index.html). Documentation can be found online on the [WIN open science website](https://open.win.ox.ac.uk/pages/fsl/fsl_mrs/).
For each of the wrapper scripts above, simply type `<name_of_script> --help` to get the usage. For each of the wrapper scripts above, simply type `<name_of_script> --help` to get the usage.
......
import datetime import datetime
import fsl_mrs
date = datetime.date.today() date = datetime.date.today()
# Configuration file for the Sphinx documentation builder. # Configuration file for the Sphinx documentation builder.
...@@ -25,7 +26,7 @@ copyright = f'{date.year}, Will Clarke & Saad Jbabdi, University of Oxford, Oxfo ...@@ -25,7 +26,7 @@ copyright = f'{date.year}, Will Clarke & Saad Jbabdi, University of Oxford, Oxfo
author = 'William Clarke' author = 'William Clarke'
# The full version, including alpha/beta/rc tags # The full version, including alpha/beta/rc tags
version = '1.0.5' version = fsl_mrs.__version__
release = version release = version
# From PM's fsleyes doc # From PM's fsleyes doc
......
...@@ -30,7 +30,7 @@ By default this option will add the following basis spectra (in separate metabol ...@@ -30,7 +30,7 @@ By default this option will add the following basis spectra (in separate metabol
Additional peaks may be added int he interactive environment by calling :code:`add_MM_peaks` with optional arguments to override the defaults. Additional peaks may be added int he interactive environment by calling :code:`add_MM_peaks` with optional arguments to override the defaults.
References References
========== ~~~~~~~~~~
.. [CUDA12] Cudalbu C, Mlynárik V, Gruetter R. Handling Macromolecule Signals in the Quantification of the Neurochemical Profile. Journal of Alzheimer’s Disease 2012;31:S101–S115 doi: 10.3233/JAD-2012-120100. .. [CUDA12] Cudalbu C, Mlynárik V, Gruetter R. Handling Macromolecule Signals in the Quantification of the Neurochemical Profile. Journal of Alzheimer’s Disease 2012;31:S101–S115 doi: 10.3233/JAD-2012-120100.
......
This diff is collapsed.
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
# Author: Saad Jbabdi <saad@fmrib.ox.ac.uk> # Author: Saad Jbabdi <saad@fmrib.ox.ac.uk>
# Will Clarke <william.clarke@ndcn.ox.ac.uk> # Will Clarke <william.clarke@ndcn.ox.ac.uk>
# #
# Copyright (C) 2020 University of Oxford # Copyright (C) 2020 University of Oxford
# SHBASECOPYRIGHT # SHBASECOPYRIGHT
import numpy as np import numpy as np
...@@ -13,42 +13,49 @@ from fsl_mrs.core import MRS ...@@ -13,42 +13,49 @@ from fsl_mrs.core import MRS
from fsl_mrs.utils import mrs_io, misc from fsl_mrs.utils import mrs_io, misc
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import nibabel as nib import nibabel as nib
from fsl_mrs.utils.mrs_io.fsl_io import saveNIFTI from fsl_mrs.utils.mrs_io.fsl_io import saveNIFTI, readNIFTI
class MRSI(object): class MRSI(object):
def __init__(self,FID,header,mask=None,basis=None,names=None,basis_hdr=None,H2O=None): def __init__(self, FID, header,
mask=None, basis=None, names=None,
basis_hdr=None, H2O=None):
# process mask # process mask
if mask is None: if mask is None:
mask = np.full(FID.shape,True) mask = np.full(FID.shape, True)
elif mask.shape[0:3]==FID.shape[0:3]: elif mask.shape[0:3] == FID.shape[0:3]:
mask = mask!=0.0 mask = mask != 0.0
else: else:
raise ValueError(f'Mask must be None or numpy array of the same shape as FID. Mask {mask.shape[0:3]}, FID {FID.shape[0:3]}.') raise ValueError(f'Mask must be None or numpy'
f' array of the same shape'
f' as FID. Mask {mask.shape[0:3]},'
f' FID {FID.shape[0:3]}.')
# process H2O # process H2O
if H2O is None: if H2O is None:
H2O = np.full(FID.shape,None) H2O = np.full(FID.shape, None)
elif H2O.shape!=FID.shape: elif H2O.shape != FID.shape:
raise ValueError('H2O must be None or numpy array of the same shape as FID.') raise ValueError('H2O must be None or numpy array '
'of the same shape as FID.')
# Load into properties # Load into properties
self.data = FID self.data = FID
self.H2O = H2O self.H2O = H2O
self.mask = mask self.mask = mask
self.header = header self.header = header
# Basis # Basis
self.basis = basis self.basis = basis
self.names = names self.names = names
self.basis_hdr = basis_hdr self.basis_hdr = basis_hdr
# tissue segmentation # tissue segmentation
self.csf = None self.csf = None
self.wm = None self.wm = None
self.gm = None self.gm = None
self.tissue_seg_loaded = False self.tissue_seg_loaded = False
# Helpful properties # Helpful properties
self.spatial_shape = self.data.shape[:3] self.spatial_shape = self.data.shape[:3]
...@@ -69,31 +76,33 @@ class MRSI(object): ...@@ -69,31 +76,33 @@ class MRSI(object):
self.ind_scaling = None self.ind_scaling = None
self._store_scalings = None self._store_scalings = None
def __iter__(self): def __iter__(self):
shape = self.data.shape shape = self.data.shape
self._store_scalings = [] self._store_scalings = []
for idx in np.ndindex(shape[:3]): for idx in np.ndindex(shape[:3]):
if self.mask[idx]: if self.mask[idx]:
mrs_out = MRS(FID=self.data[idx], mrs_out = MRS(FID=self.data[idx],
header=self.header, header=self.header,
basis=self.basis, basis=self.basis,
names=self.names, names=self.names,
basis_hdr=self.basis_hdr, basis_hdr=self.basis_hdr,
H2O=self.H2O[idx]) H2O=self.H2O[idx])
self._process_mrs(mrs_out) self._process_mrs(mrs_out)
self._store_scalings.append(mrs_out.scaling) self._store_scalings.append(mrs_out.scaling)
if self.tissue_seg_loaded: if self.tissue_seg_loaded:
tissue_seg = {'CSF':self.csf[idx],'WM':self.wm[idx],'GM':self.gm[idx]} tissue_seg = {'CSF': self.csf[idx],
'WM': self.wm[idx],
'GM': self.gm[idx]}
else: else:
tissue_seg = None tissue_seg = None
yield mrs_out,idx,tissue_seg yield mrs_out, idx, tissue_seg
def get_indicies_in_order(self,mask=True): def get_indicies_in_order(self, mask=True):
"""Return a list of iteration indicies in order""" """Return a list of iteration indicies in order"""
out = [] out = []
shape = self.data.shape shape = self.data.shape
for idx in np.ndindex(shape[:3]): for idx in np.ndindex(shape[:3]):
...@@ -104,30 +113,34 @@ class MRSI(object): ...@@ -104,30 +113,34 @@ class MRSI(object):
out.append(idx) out.append(idx)
return out return out
def get_scalings_in_order(self,mask=True): def get_scalings_in_order(self, mask=True):
"""Return a list of MRS object scalings in order""" """Return a list of MRS object scalings in order"""
if self._store_scalings is None: if self._store_scalings is None:
raise ValueError('Fetch mrs by iterable first.') raise ValueError('Fetch mrs by iterable first.')
else: else:
return self._store_scalings return self._store_scalings
def mrs_by_index(self,index): def mrs_by_index(self, index):
mrs_out = MRS(FID=self.data[index[0],index[1],index[2],:], ''' Return MRS object by index (tuple - x,y,z).'''
header=self.header, mrs_out = MRS(FID=self.data[index[0], index[1], index[2], :],
basis=self.basis, header=self.header,
names=self.names, basis=self.basis,
basis_hdr=self.basis_hdr, names=self.names,
H2O=self.H2O[index[0],index[1],index[2],:]) basis_hdr=self.basis_hdr,
H2O=self.H2O[index[0], index[1], index[2], :])
self._process_mrs(mrs_out) self._process_mrs(mrs_out)
return mrs_out return mrs_out
def mrs_from_average(self): def mrs_from_average(self):
FID = misc.volume_to_list(self.data,self.mask) '''
H2O = misc.volume_to_list(self.H2O,self.mask) Return average of all masked voxels
FID = sum(FID)/len(FID) as a single MRS object.
H2O = sum(H2O)/len(H2O) '''
FID = misc.volume_to_list(self.data, self.mask)
H2O = misc.volume_to_list(self.H2O, self.mask)
FID = sum(FID) / len(FID)
H2O = sum(H2O) / len(H2O)
mrs_out = MRS(FID=FID, mrs_out = MRS(FID=FID,
header=self.header, header=self.header,
basis=self.basis, basis=self.basis,
...@@ -137,15 +150,20 @@ class MRSI(object): ...@@ -137,15 +150,20 @@ class MRSI(object):
self._process_mrs(mrs_out) self._process_mrs(mrs_out)
return mrs_out return mrs_out
def seg_by_index(self, index):
def seg_by_index(self,index): '''Return segmentation information by index.'''
if self.tissue_seg_loaded: if self.tissue_seg_loaded:
return {'CSF':self.csf[index],'WM':self.wm[index],'GM':self.gm[index]} return {'CSF': self.csf[index],
'WM': self.wm[index],
'GM': self.gm[index]}
else: else:
raise ValueError('Load tissue segmentation first.') raise ValueError('Load tissue segmentation first.')
def _process_mrs(self,mrs): def _process_mrs(self, mrs):
''' Process (conjugate, rescale)
basis and FID and apply basis operations
to all voxels.
'''
if self.basis is not None: if self.basis is not None:
if self.conj_basis: if self.conj_basis:
mrs.conj_Basis() mrs.conj_Basis()
...@@ -153,7 +171,7 @@ class MRSI(object): ...@@ -153,7 +171,7 @@ class MRSI(object):
pass pass
else: else:
mrs.check_Basis(repair=True) mrs.check_Basis(repair=True)
mrs.keep(self.keep) mrs.keep(self.keep)
mrs.ignore(self.ignore) mrs.ignore(self.ignore)
...@@ -165,124 +183,147 @@ class MRSI(object): ...@@ -165,124 +183,147 @@ class MRSI(object):
mrs.check_FID(repair=True) mrs.check_FID(repair=True)
if self.rescale: if self.rescale:
mrs.rescaleForFitting(ind_scaling=self.ind_scaling) mrs.rescaleForFitting(ind_scaling=self.ind_scaling)
def plot(self,mask=True,ppmlim=(0.2,4.2)): def plot(self, mask=True, ppmlim=(0.2, 4.2)):
'''Plot (masked) grid of spectra.'''
if mask: if mask:
mask_indicies = np.where(self.mask) mask_indicies = np.where(self.mask)
else: else:
mask_indicies = np.where(np.full(self.mask.shape,True)) mask_indicies = np.where(np.full(self.mask.shape, True))
dim1 = np.asarray((np.min(mask_indicies[0]),np.max(mask_indicies[0]))) dim1 = np.asarray((np.min(mask_indicies[0]), np.max(mask_indicies[0])))
dim2 = np.asarray((np.min(mask_indicies[1]),np.max(mask_indicies[1]))) dim2 = np.asarray((np.min(mask_indicies[1]), np.max(mask_indicies[1])))
dim3 = np.asarray((np.min(mask_indicies[2]),np.max(mask_indicies[2]))) dim3 = np.asarray((np.min(mask_indicies[2]), np.max(mask_indicies[2])))
size1 = 1+ dim1[1]-dim1[0] size1 = 1 + dim1[1] - dim1[0]
size2 = 1+ dim2[1]-dim2[0] size2 = 1 + dim2[1] - dim2[0]
size3 = 1+ dim3[1]-dim3[0] size3 = 1 + dim3[1] - dim3[0]
ar1 = size1/(size1+size2) ar1 = size1 / (size1 + size2)
ar2 = size2/(size1+size2) ar2 = size2 / (size1 + size2)
for sDx in range(size3): for sDx in range(size3):
fig,axes = plt.subplots(size1,size2,figsize=(20*ar2,20*ar1)) fig, axes = plt.subplots(size1, size2, figsize=(20 * ar2, 20 * ar1))
for i,j,k in zip(*mask_indicies): for i, j, k in zip(*mask_indicies):
if (not self.mask[i,j,k]) and mask: if (not self.mask[i, j, k]) and mask:
continue continue
ii = i - dim1[0] ii = i - dim1[0]
jj = j - dim2[0] jj = j - dim2[0]
ax = axes[ii,jj] ax = axes[ii, jj]
mrs = self.mrs_by_index([i,j,k]) mrs = self.mrs_by_index([i, j, k])
ax.plot(mrs.getAxes(ppmlim=ppmlim),np.real(mrs.getSpectrum(ppmlim=ppmlim))) ax.plot(mrs.getAxes(ppmlim=ppmlim), np.real(mrs.get_spec(ppmlim=ppmlim)))
ax.invert_xaxis() ax.invert_xaxis()
ax.set_xticks([]) ax.set_xticks([])
ax.set_yticks([]) ax.set_yticks([])
plt.subplots_adjust(left = 0.03, # the left side of the subplots of the figure plt.subplots_adjust(left=0.03, # the left side of the subplots of the figure
right = 0.97, # the right side of the subplots of the figure right=0.97, # the right side of the subplots of the figure
bottom = 0.01, # the bottom of the subplots of the figure bottom=0.01, # the bottom of the subplots of the figure
top = 0.95, # the top of the subplots of the figure top=0.95, # the top of the subplots of the figure
wspace = 0, # the amount of width reserved for space between subplots, wspace=0, # the amount of width reserved for space between subplots,
hspace = 0) hspace=0)
fig.suptitle(f'Slice {k}') fig.suptitle(f'Slice {sDx}')
plt.show() plt.show()
def __str__(self): def __str__(self):
return f'MRSI with shape {self.data.shape}\nNumber of voxels = {self.num_voxels}\nNumber of masked voxels = {self.num_masked_voxels}' return f'MRSI with shape {self.data.shape}\n' \
f'Number of voxels = {self.num_voxels}\n' \
f'Number of masked voxels = {self.num_masked_voxels}'
def __repr__(self): def __repr__(self):
return str(self) return str(self)
def set_mask(self,mask): def set_mask(self, mask):
""" Load mask as numpy array.""" """ Load mask as numpy array."""
if mask is None: if mask is None:
mask = np.full(self.data.shape,True) mask = np.full(self.data.shape, True)
elif mask.shape[0:3]==self.data.shape[0:3]: elif mask.shape[0:3]