diff --git a/.gitignore b/.gitignore index e1046173..30c97a25 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,5 @@ nosetests.xml coverage.xml *.pyc phantoms/MR_XCAT_qMRI/*.json -phantoms/MR_XCAT_qMRI/*.txt \ No newline at end of file +phantoms/MR_XCAT_qMRI/*.txt + diff --git a/src/standardized wip/IAR_LU_linear.py b/src/standardized wip/IAR_LU_linear.py index 7ff310d0..602ba50b 100644 --- a/src/standardized wip/IAR_LU_linear.py +++ b/src/standardized wip/IAR_LU_linear.py @@ -26,7 +26,7 @@ class IAR_LU_linear(OsipiBase): 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? + accepted_dimensions = (1,1) #(min dimension, max dimension) def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=None, weighting=None, stats=False): """ @@ -38,8 +38,6 @@ def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=Non """ super(IAR_LU_linear, self).__init__(bvalues, thresholds, bounds, initial_guess) - # Check the inputs - # Initialize the algorithm if self.bvalues is not None: bvec = np.zeros((self.bvalues.size, 3)) diff --git a/src/standardized/ETP_SRI_LinearFitting.py b/src/standardized/ETP_SRI_LinearFitting.py index 8494fc18..3777afbb 100644 --- a/src/standardized/ETP_SRI_LinearFitting.py +++ b/src/standardized/ETP_SRI_LinearFitting.py @@ -25,8 +25,7 @@ class ETP_SRI_LinearFitting(OsipiBase): required_bounds_optional = True # Bounds may not be required but are optional required_initial_guess = False required_initial_guess_optional = False - accepted_dimensions = 1 - # Not sure how to define this for the number of accepted dimensions. Perhaps like the thresholds, at least and at most? + accepted_dimensions = (1,1) #(min dimension, max dimension) # Supported inputs in the standardized class supported_bounds = False @@ -41,7 +40,6 @@ def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=Non Our OsipiBase object could contain functions that compare the inputs with the requirements. """ - super(ETP_SRI_LinearFitting, self).__init__(bvalues, thresholds, bounds, initial_guess) if bounds is not None: print('warning, bounds from wrapper are not (yet) used in this algorithm') @@ -52,10 +50,8 @@ def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=Non # defined with initials? self.ETP_weighting = weighting self.ETP_stats = stats - - # Check the inputs - + def ivim_fit(self, signals, bvalues=None, linear_fit_option=False, **kwargs): """Perform the IVIM fit @@ -67,6 +63,7 @@ def ivim_fit(self, signals, bvalues=None, linear_fit_option=False, **kwargs): Returns: _type_: _description_ """ + signals[signals<0.0000001]=0.0000001 if bvalues is None: bvalues = self.bvalues diff --git a/src/standardized/IAR_LU_biexp.py b/src/standardized/IAR_LU_biexp.py index a9147839..7e638465 100644 --- a/src/standardized/IAR_LU_biexp.py +++ b/src/standardized/IAR_LU_biexp.py @@ -26,7 +26,7 @@ class IAR_LU_biexp(OsipiBase): 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? + accepted_dimensions = (1,1) #(min dimension, max dimension) # Supported inputs in the standardized class supported_bounds = True @@ -46,7 +46,6 @@ def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=Non print('warning, bounds from wrapper are not (yet) used in this algorithm') self.use_bounds = False self.use_initial_guess = False - # Check the inputs # Initialize the algorithm if self.bvalues is not None: @@ -69,7 +68,7 @@ def ivim_fit(self, signals, bvalues, **kwargs): Returns: _type_: _description_ """ - + if self.IAR_algorithm is None: if bvalues is None: bvalues = self.bvalues @@ -101,7 +100,7 @@ def ivim_fit_full_volume(self, signals, bvalues, **kwargs): Returns: _type_: _description_ """ - + if self.IAR_algorithm is None: if bvalues is None: bvalues = self.bvalues diff --git a/src/standardized/IAR_LU_modified_mix.py b/src/standardized/IAR_LU_modified_mix.py index fe6fb101..4712d7c8 100644 --- a/src/standardized/IAR_LU_modified_mix.py +++ b/src/standardized/IAR_LU_modified_mix.py @@ -26,7 +26,7 @@ class IAR_LU_modified_mix(OsipiBase): 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? + accepted_dimensions = (1,1) #(min dimension, max dimension) # Supported inputs in the standardized class supported_bounds = True @@ -46,7 +46,6 @@ def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=Non print('warning, bounds from wrapper are not (yet) used in this algorithm') self.use_bounds = False self.use_initial_guess = False - # Check the inputs # Initialize the algorithm if self.bvalues is not None: @@ -69,7 +68,7 @@ def ivim_fit(self, signals, bvalues, **kwargs): Returns: _type_: _description_ """ - + if self.IAR_algorithm is None: if bvalues is None: bvalues = self.bvalues diff --git a/src/standardized/IAR_LU_modified_topopro.py b/src/standardized/IAR_LU_modified_topopro.py index b52cf948..ffe39427 100644 --- a/src/standardized/IAR_LU_modified_topopro.py +++ b/src/standardized/IAR_LU_modified_topopro.py @@ -26,7 +26,7 @@ class IAR_LU_modified_topopro(OsipiBase): 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? + accepted_dimensions = (1,1) #(min dimension, max dimension) # Supported inputs in the standardized class supported_bounds = True @@ -46,7 +46,6 @@ def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=Non print('warning, bounds from wrapper are not (yet) used in this algorithm') self.use_bounds = False self.use_initial_guess = False - # Check the inputs # Initialize the algorithm if self.bvalues is not None: diff --git a/src/standardized/IAR_LU_segmented_2step.py b/src/standardized/IAR_LU_segmented_2step.py index 2fa24c39..224d609a 100644 --- a/src/standardized/IAR_LU_segmented_2step.py +++ b/src/standardized/IAR_LU_segmented_2step.py @@ -26,7 +26,7 @@ class IAR_LU_segmented_2step(OsipiBase): 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? + accepted_dimensions = (1,1) #(min dimension, max dimension) # Supported inputs in the standardized class supported_bounds = True @@ -46,7 +46,6 @@ def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=Non print('warning, bounds from wrapper are not (yet) used in this algorithm') self.use_bounds = False self.use_initial_guess = False - # Check the inputs # Initialize the algorithm if self.bvalues is not None: @@ -70,6 +69,7 @@ def ivim_fit(self, signals, bvalues, thresholds=None, **kwargs): _type_: _description_ """ print(thresholds) + if self.IAR_algorithm is None: if bvalues is None: diff --git a/src/standardized/IAR_LU_segmented_3step.py b/src/standardized/IAR_LU_segmented_3step.py index 8e424f6f..85b44f68 100644 --- a/src/standardized/IAR_LU_segmented_3step.py +++ b/src/standardized/IAR_LU_segmented_3step.py @@ -26,7 +26,7 @@ class IAR_LU_segmented_3step(OsipiBase): 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? + accepted_dimensions = (1,1) #(min dimension, max dimension) # Supported inputs in the standardized class supported_bounds = True @@ -46,7 +46,6 @@ def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=Non print('warning, bounds from wrapper are not (yet) used in this algorithm') self.use_bounds = False self.use_initial_guess = False - # Check the inputs # Initialize the algorithm if self.bvalues is not None: diff --git a/src/standardized/IAR_LU_subtracted.py b/src/standardized/IAR_LU_subtracted.py index 39f72f5f..fb7c1995 100644 --- a/src/standardized/IAR_LU_subtracted.py +++ b/src/standardized/IAR_LU_subtracted.py @@ -26,7 +26,7 @@ class IAR_LU_subtracted(OsipiBase): 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? + accepted_dimensions = (1,1) #(min dimension, max dimension) # Supported inputs in the standardized class supported_bounds = True @@ -46,7 +46,6 @@ def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=Non print('warning, bounds from wrapper are not (yet) used in this algorithm') self.use_bounds = False self.use_initial_guess = False - # Check the inputs # Initialize the algorithm if self.bvalues is not None: @@ -70,6 +69,7 @@ def ivim_fit(self, signals, bvalues, **kwargs): _type_: _description_ """ + if self.IAR_algorithm is None: if bvalues is None: bvalues = self.bvalues diff --git a/src/standardized/OGC_AmsterdamUMC_Bayesian_biexp.py b/src/standardized/OGC_AmsterdamUMC_Bayesian_biexp.py index a248166d..8355d3ac 100644 --- a/src/standardized/OGC_AmsterdamUMC_Bayesian_biexp.py +++ b/src/standardized/OGC_AmsterdamUMC_Bayesian_biexp.py @@ -19,13 +19,12 @@ class OGC_AmsterdamUMC_Bayesian_biexp(OsipiBase): # 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_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? + accepted_dimensions = (1,1) #(min dimension, max dimension) accepts_priors = True @@ -86,6 +85,9 @@ def ivim_fit(self, signals, bvalues, initial_guess=None, **kwargs): Returns: _type_: _description_ """ + + if initial_guess is not None and len(initial_guess) == 4: + self.initial_guess = initial_guess bvalues=np.array(bvalues) epsilon = 0.000001 @@ -100,5 +102,6 @@ def ivim_fit(self, signals, bvalues, initial_guess=None, **kwargs): results["D"] = fit_results[0] results["f"] = fit_results[1] results["Dp"] = fit_results[2] + results["S0"] = fit_results[3] return results \ No newline at end of file diff --git a/src/standardized/OGC_AmsterdamUMC_biexp.py b/src/standardized/OGC_AmsterdamUMC_biexp.py index 49a16b27..c59a7f3e 100644 --- a/src/standardized/OGC_AmsterdamUMC_biexp.py +++ b/src/standardized/OGC_AmsterdamUMC_biexp.py @@ -19,13 +19,12 @@ class OGC_AmsterdamUMC_biexp(OsipiBase): # 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_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? + accepted_dimensions = (1,1) #(min dimension, max dimension) # Supported inputs in the standardized class @@ -81,5 +80,6 @@ def ivim_fit(self, signals, bvalues, **kwargs): results["D"] = fit_results[0] results["f"] = fit_results[1] results["Dp"] = fit_results[2] + results["S0"] = fit_results[3] return results \ No newline at end of file diff --git a/src/standardized/OGC_AmsterdamUMC_biexp_segmented.py b/src/standardized/OGC_AmsterdamUMC_biexp_segmented.py index 68f6e5bc..a82ed290 100644 --- a/src/standardized/OGC_AmsterdamUMC_biexp_segmented.py +++ b/src/standardized/OGC_AmsterdamUMC_biexp_segmented.py @@ -19,13 +19,12 @@ class OGC_AmsterdamUMC_biexp_segmented(OsipiBase): # Algorithm requirements required_bvalues = 4 - required_thresholds = [1, - 1] # Interval from "at least" to "at most", in case submissions allow a custom number of thresholds + required_thresholds = [1,1] # 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? + accepted_dimensions = (1,1) #(min dimension, max dimension) # Supported inputs in the standardized class @@ -74,7 +73,9 @@ def ivim_fit(self, signals, bvalues, **kwargs): Returns: _type_: _description_ """ - + + if self.initial_guess is not None and len(self.initial_guess) == 4: + self.initial_guess = self.initial_guess bvalues=np.array(bvalues) fit_results = self.OGC_algorithm(bvalues, signals, bounds=self.bounds, cutoff=self.thresholds, p0=self.initial_guess) @@ -82,5 +83,6 @@ def ivim_fit(self, signals, bvalues, **kwargs): results["D"] = fit_results[0] results["f"] = fit_results[1] results["Dp"] = fit_results[2] + results["S0"] = fit_results[3] return results \ No newline at end of file diff --git a/src/standardized/OJ_GU_seg.py b/src/standardized/OJ_GU_seg.py index 7e49bab4..6b295efa 100644 --- a/src/standardized/OJ_GU_seg.py +++ b/src/standardized/OJ_GU_seg.py @@ -24,7 +24,7 @@ class OJ_GU_seg(OsipiBase): required_bounds_optional = False # Bounds may not be required but are optional required_initial_guess = False required_initial_guess_optional = False - accepted_dimensions = 1 # Not sure how to define this for the number of accepted dimensions. Perhaps like the thresholds, at least and at most? + accepted_dimensions = (1,1) #(min dimension, max dimension) # Supported inputs in the standardized class supported_bounds = False @@ -44,7 +44,6 @@ def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=Non print('warning, bounds from wrapper are not (yet) used in this algorithm') self.use_bounds = False self.use_initial_guess = False - # Check the inputs # Initialize the algorithm diff --git a/src/standardized/PV_MUMC_biexp.py b/src/standardized/PV_MUMC_biexp.py index 821f1345..c17e108a 100644 --- a/src/standardized/PV_MUMC_biexp.py +++ b/src/standardized/PV_MUMC_biexp.py @@ -21,7 +21,7 @@ class PV_MUMC_biexp(OsipiBase): 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? + accepted_dimensions = (1,1) #(min dimension, max dimension) # Supported inputs in the standardized class supported_bounds = True diff --git a/src/standardized/PvH_KB_NKI_IVIMfit.py b/src/standardized/PvH_KB_NKI_IVIMfit.py index 27d735bf..5054dd9d 100644 --- a/src/standardized/PvH_KB_NKI_IVIMfit.py +++ b/src/standardized/PvH_KB_NKI_IVIMfit.py @@ -19,13 +19,12 @@ class PvH_KB_NKI_IVIMfit(OsipiBase): # 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_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 = False # Bounds may not be required but are optional required_initial_guess = False required_initial_guess_optional =False - accepted_dimensions = 1 # Not sure how to define this for the number of accepted dimensions. Perhaps like the thresholds, at least and at most? + accepted_dimensions = (1,1) #(min dimension, max dimension) # Supported inputs in the standardized class supported_bounds = False diff --git a/src/wrappers/OsipiBase.py b/src/wrappers/OsipiBase.py index 6c5d380e..219723e0 100644 --- a/src/wrappers/OsipiBase.py +++ b/src/wrappers/OsipiBase.py @@ -16,6 +16,17 @@ def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=Non self.initial_guess = np.asarray(initial_guess) if initial_guess is not None else None self.use_bounds = True self.use_initial_guess = True + + # Validate the inputs + if self.bvalues is not None: + self.osipi_check_required_bvalues() + if self.thresholds is not None: + self.osipi_check_required_thresholds() + if self.bounds is not None: + self.osipi_check_required_bounds() + if self.initial_guess is not None: + self.osipi_check_required_initial_guess() + # If the user inputs an algorithm to OsipiBase, it is intereprete as initiating # an algorithm object with that name. if algorithm: @@ -53,7 +64,9 @@ def osipi_fit(self, data, bvalues=None, **kwargs): """Fits the data with the bvalues Returns [S0, f, Dstar, D] """ - + self.data = np.asarray(data) + self.osipi_validate_inputs() + # We should first check whether the attributes in the __init__ are not None # Then check if they are input here, if they are, these should overwrite the attributes use_bvalues = bvalues if bvalues is not None else self.bvalues @@ -123,6 +136,9 @@ def osipi_fit_full_volume(self, data, bvalues=None, **kwargs): Returns: results (dict): Dict with key each containing an array which is a parametric map. """ + + self.data = np.asarray(data) + self.osipi_validate_inputs() try: use_bvalues = bvalues if bvalues is not None else self.bvalues @@ -201,6 +217,14 @@ def osipi_print_requirements(self): else: print(f"Initial guess required: {self.required_initial_guess} and is not optional") + def osipi_validate_inputs(self): + """Validates the inputs of the algorithm.""" + self.osipi_accepts_dimension(self.data.ndim) + self.osipi_check_required_bvalues() + self.osipi_check_required_thresholds() + self.osipi_check_required_bounds() + self.osipi_check_required_initial_guess() + def osipi_accepted_dimensions(self): """The array of accepted dimensions e.g. @@ -208,61 +232,45 @@ def osipi_accepted_dimensions(self): (True, True, False, False, False, False) """ - #return (False,) * 6 - return True + return getattr(self, 'accepted_dimensions', (1, 3)) def osipi_accepts_dimension(self, dim): - """Query if the selection dimension is fittable""" - - #accepted = self.accepted_dimensions() - #if dim < 0 or dim > len(accepted): - #return False - #return accepted[dim] - return True + """Check if the spatial dimensions (excluding b-values) are supported.""" + spatial_dim = dim - 1 # Exclude last axis (b-values) + min_dim, max_dim = self.osipi_accepted_dimensions() + if not (min_dim <= spatial_dim <= max_dim): + raise ValueError( + f"Spatial dimensions {spatial_dim}D not supported. " + f"Requires {min_dim}-{max_dim}D." + ) def osipi_check_required_bvalues(self): """Checks if the input bvalues fulfil the algorithm requirements""" - - #if self.bvalues.size < self.required_bvalues: - #print("Conformance error: Number of b-values.") - #return False - #else: - #return True - return True + if not hasattr(self, "required_bvalues"): + raise AttributeError("required_bvalues not defined for this algorithm") + if self.bvalues is None: + raise ValueError("bvalues are not provided") + if len(self.bvalues) < self.required_bvalues: + raise ValueError(f"Atleast {self.required_bvalues} are required, but only {len(self.bvalues)} were provided") def osipi_check_required_thresholds(self): """Checks if the number of input thresholds fulfil the algorithm requirements""" - - #if (len(self.thresholds) < self.required_thresholds[0]) or (len(self.thresholds) > self.required_thresholds[1]): - #print("Conformance error: Number of thresholds.") - #return False - #else: - #return True - return True + if not hasattr(self, "required_thresholds"): + raise AttributeError("required_thresholds is not defined for this algorithm") + if self.thresholds is None: + raise ValueError("thresholds are not provided") + if len(self.thresholds) < self.required_thresholds[0] and len(self.thresholds) > self.required_thresholds[1]: + raise ValueError(f"Thresholds should be between {self.required_thresholds[0]} and {self.required_thresholds[1]} but {len(self.thresholds)} were provided") def osipi_check_required_bounds(self): """Checks if input bounds fulfil the algorithm requirements""" - #if self.required_bounds is True and self.bounds is None: - #print("Conformance error: Bounds.") - #return False - #else: - #return True - return True + if self.required_bounds is False and self.bounds is None: + raise ValueError("bounds are required but not provided") def osipi_check_required_initial_guess(self): """Checks if input initial guess fulfil the algorithm requirements""" - - #if self.required_initial_guess is True and self.initial_guess is None: - #print("Conformance error: Initial guess") - #return False - #else: - #return True - return True - - - def osipi_check_required_bvalues(): - """Minimum number of b-values required""" - pass + if self.required_initial_guess is False and self.initial_guess is None: + raise ValueError("initial_guess are required but not provided") def osipi_author(): """Author identification"""