from src.wrappers.OsipiBase import OsipiBase
import numpy as np
import os
from super_ivim_dc.train import train
from pathlib import Path
from super_ivim_dc.infer import infer_from_signal
import warnings
[docs]
class Super_IVIM_DC(OsipiBase):
"""
Supervised deep learnt fitting algorithm by Moti Freiman and Noam Korngut, TechnionIIT
"""
# I'm thinking that we define default attributes for each submission like this
# And in __init__, we can call the OsipiBase control functions to check whether
# the user inputs fulfil the requirements
# Some basic stuff that identifies the algorithm
id_author = "Moti Freiman and Noam Korngut, TechnIIT"
id_algorithm_type = "Supervised Deep learnt bi-exponential fit with data consistency"
id_return_parameters = "f, D*, D, S0"
id_units = "seconds per milli metre squared or milliseconds per micro metre squared"
# Algorithm requirements
required_bvalues = 4
required_thresholds = [0,
0] # Interval from "at least" to "at most", in case submissions allow a custom number of thresholds
required_bounds = False
required_bounds_optional = True # Bounds may not be required but are optional
required_initial_guess = False
required_initial_guess_optional = True
accepted_dimensions = 1 # Not sure how to define this for the number of accepted dimensions. Perhaps like the thresholds, at least and at most?
# Supported inputs in the standardized class
supported_bounds = True
supported_initial_guess = True
supported_thresholds = False
def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=None, fitS0=True, SNR = None):
"""
Everything this algorithm requires should be implemented here.
Number of segmentation thresholds, bounds, etc.
Our OsipiBase object could contain functions that compare the inputs with
the requirements.
"""
if bvalues is None:
raise ValueError("for deep learning models, bvalues need defining at initiaition")
super(Super_IVIM_DC, self).__init__(bvalues=bvalues, bounds=bounds, initial_guess=initial_guess)
self.fitS0=fitS0
self.bvalues=np.array(bvalues)
self.initialize(bounds, initial_guess, fitS0, SNR)
[docs]
def initialize(self, bounds, initial_guess, fitS0, SNR, working_dir=os.getcwd(),ivimnet_filename='ivimnet',super_ivim_dc_filename='super_ivim_dc'):
if SNR is None:
warnings.warn('No SNR indicated. Data simulated with SNR = 100')
SNR=100
self.fitS0=fitS0
self.use_initial_guess = False
self.use_bounds = False
self.deep_learning = True
self.supervised = True
# Additional options
self.stochastic = True
modeldir = Path(working_dir) # Ensure it's a Path object
modeldir = modeldir / "models"
modeldir.mkdir(parents=True, exist_ok=True)
working_dir: str = str(modeldir)
super_ivim_dc_filename: str = super_ivim_dc_filename # do not include .pt
ivimnet_filename: str = ivimnet_filename # do not include .pt
self.super_ivim_dc_filename=super_ivim_dc_filename
self.ivimnet_filename=ivimnet_filename
self.working_dir=working_dir
train(
SNR=SNR,
bvalues=self.bvalues,
super_ivim_dc=True,
work_dir=self.working_dir,
super_ivim_dc_filename=self.super_ivim_dc_filename,
ivimnet_filename=ivimnet_filename,
verbose=False,
ivimnet=False
)
[docs]
def ivim_fit(self, signals, bvalues, **kwargs):
"""Perform the IVIM fit
Args:
signals (array-like)
bvalues (array-like, optional): b-values for the signals. If None, self.bvalues will be used. Default is None.
Returns:
results: a dictionary containing "d", "f", and "Dp".
"""
if not np.array_equal(bvalues, self.bvalues):
raise ValueError("bvalue list at fitting must be identical as the one at initiation, otherwise it will not run")
if np.shape(np.shape(signals)) == (1,):
signals=signals[np.newaxis, :]
Dp, Dt, f, S0_superivimdc = infer_from_signal(
signal=signals,
bvalues=self.bvalues,
model_path=f"{self.working_dir}/{self.super_ivim_dc_filename}.pt",
)
# fallback for empty arrays
if Dp.size == 0:
Dp = 0.0
if Dt.size == 0:
Dt = 0.0
if f.size == 0:
f = 0.0
results = {}
results["D"] = Dt
results["f"] = f
results["Dp"] = Dp
return results
[docs]
def ivim_fit_full_volume(self, signals, bvalues, retrain_on_input_data=False, **kwargs):
"""Perform the IVIM fit
Args:
signals (array-like)
bvalues (array-like): b-values for the signals. If None, self.bvalues will be used. Default is None.
Returns:
_type_: _description_
"""
if not np.array_equal(bvalues, self.bvalues):
raise ValueError("bvalue list at fitting must be identical as the one at initiation, otherwise it will not run")
nanmask = np.any(np.isnan(signals),axis=-1)
signals,shape = self.reshape_to_voxelwise(signals)
Dp, Dt, f, S0_superivimdc = infer_from_signal(
signal=signals,
bvalues=self.bvalues,
model_path=f"{self.working_dir}/{self.super_ivim_dc_filename}.pt",
)
results = {}
for name,par in zip(["D","f","Dp"],[Dt,f,Dp]):
parmap = np.full(shape[:-1],np.nan)
parmap[~nanmask] = par
results[name] = parmap
# results["f"] = np.reshape(f,shape[:-1])
# results["Dp"] = np.reshape(Dp,shape[:-1])
return results
[docs]
def reshape_to_voxelwise(self, data):
"""
reshapes multi-D input (spatial dims, bvvalue) data to 2D voxel-wise array
Args:
data (array): mulit-D array (data x b-values)
Returns:
out (array): 2D array (voxel x b-value)
"""
B = data.shape[-1]
voxels = int(np.prod(data.shape[:-1])) # e.g., X*Y*Z
return data.reshape(voxels, B), data.shape