From f9161f23425d592d7d5e39fc59c5a0bced9e9898 Mon Sep 17 00:00:00 2001 From: Oliver Gurney-Champion Date: Wed, 28 May 2025 08:53:43 +0200 Subject: [PATCH 01/18] Matlab installation + adding Amita's code to wrapper Unit test will fail. Mainly checking whether matlab is called --- .github/workflows/unit_test.yml | 6 ++ .../MRI-QAMPER_IVIM/IVIM_standard_bcin.m | 1 + .../ASD_MemorialSloanKettering_QAMPER_IVIM.py | 97 +++++++++++++++++++ tests/IVIMmodels/unit_tests/algorithms.json | 1 + 4 files changed, 105 insertions(+) create mode 100644 src/standardized/ASD_MemorialSloanKettering_QAMPER_IVIM.py diff --git a/.github/workflows/unit_test.yml b/.github/workflows/unit_test.yml index c5475ed2..946a340b 100644 --- a/.github/workflows/unit_test.yml +++ b/.github/workflows/unit_test.yml @@ -30,6 +30,12 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements.txt + - name: Check out repository + uses: actions/checkout@v4 + - name: Set up MATLAB + uses: matlab-actions/setup-matlab@v2 + - name: Run build + uses: matlab-actions/run-build@v2 - name: Test with pytest run: | pip install pytest pytest-cov diff --git a/src/original/ASD_MemorialSloanKettering/MRI-QAMPER_IVIM/IVIM_standard_bcin.m b/src/original/ASD_MemorialSloanKettering/MRI-QAMPER_IVIM/IVIM_standard_bcin.m index 09828228..220980fc 100755 --- a/src/original/ASD_MemorialSloanKettering/MRI-QAMPER_IVIM/IVIM_standard_bcin.m +++ b/src/original/ASD_MemorialSloanKettering/MRI-QAMPER_IVIM/IVIM_standard_bcin.m @@ -7,6 +7,7 @@ end numVoxels = size(dwi_arr,2); +print(numVoxels) numBvals = length(bval_arr); f_lb = 0; diff --git a/src/standardized/ASD_MemorialSloanKettering_QAMPER_IVIM.py b/src/standardized/ASD_MemorialSloanKettering_QAMPER_IVIM.py new file mode 100644 index 00000000..a5a92d49 --- /dev/null +++ b/src/standardized/ASD_MemorialSloanKettering_QAMPER_IVIM.py @@ -0,0 +1,97 @@ +from src.wrappers.OsipiBase import OsipiBase +import numpy as np +import matlab.engine + +class ASD_MemorialSloanKettering_QAMPER_IVIM(OsipiBase): + """ + Bi-exponential fitting algorithm by Oliver Gurney-Champion, Amsterdam UMC + """ + + # 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 = "LoCastro, Dr. Ramesh Paudyal, Dr. Amita Shukla-Dave" + id_algorithm_type = "Bi-exponential fit" + 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): + """ + 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. + """ + #super(OGC_AmsterdamUMC_biexp, self).__init__(bvalues, bounds, initial_guess, fitS0) + super(ASD_MemorialSloanKettering_QAMPER_IVIM, self).__init__(bvalues=bvalues, bounds=bounds, initial_guess=initial_guess) + self.initialize(bounds, initial_guess) + + def algorithm(self,dwi_arr, bval_arr, LB0, UB0, x0in): + eng = matlab.engine.start_matlab() + dwi_arr = matlab.double(dwi_arr.tolist()) + bval_arr = matlab.double(bval_arr.tolist()) + LB0 = matlab.double(LB0.tolist()) + UB0 = matlab.double(UB0.tolist()) + x0in = matlab.double(x0in.tolist()) + f_arr, D_arr, Dx_arr, s0_arr, fitted_dwi_arr, RSS, rms_val, chi, AIC, BIC, R_sq = eng.IVIM_standard_bcin( + dwi_arr, bval_arr, 0.0, LB0, UB0, x0in, False, 0, 0) + eng.quit() + return D_arr, f_arr, Dx_arr, s0_arr + + def initialize(self, bounds, initial_guess): + if bounds is None: + print('warning, no bounds were defined, so algorithm-specific default bounds are used') + self.bounds=([1e-6, 0, 1e-6, 0],[0.003, 1.0, 5e-2, 5]) + else: + self.bounds=bounds + if initial_guess is None: + print('warning, no initial guesses were defined, so algorithm-specific default initial guess is used') + self.initial_guess = [0.001, 0.001, 0.2, 1] + else: + self.initial_guess = initial_guess + self.use_initial_guess = True + self.use_initial_guess = True + self.use_bounds = True + + 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: + _type_: _description_ + """ + + bvalues=np.array(bvalues) + LB = np.array(self.bounds[0]) + UB = np.array(self.bounds[1]) + + fit_results = self.algorithm(np.array(signals), bvalues, LB, UB, np.array(self.initial_guess)) + + results = {} + results["D"] = fit_results[0] + results["f"] = fit_results[1] + results["Dp"] = fit_results[2] + + return results \ No newline at end of file diff --git a/tests/IVIMmodels/unit_tests/algorithms.json b/tests/IVIMmodels/unit_tests/algorithms.json index 51a56500..c6ca198e 100644 --- a/tests/IVIMmodels/unit_tests/algorithms.json +++ b/tests/IVIMmodels/unit_tests/algorithms.json @@ -1,5 +1,6 @@ { "algorithms": [ + "ASD_MemorialSloanKettering_QAMPER_IVIM", "ETP_SRI_LinearFitting", "IAR_LU_biexp", "IAR_LU_modified_mix", From 3a5095ffec34757fc9da983bc134d653467cf0cb Mon Sep 17 00:00:00 2001 From: Oliver Gurney-Champion Date: Wed, 28 May 2025 09:34:03 +0200 Subject: [PATCH 02/18] Handle Matlab engine to remain open --- .../ASD_MemorialSloanKettering_QAMPER_IVIM.py | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/standardized/ASD_MemorialSloanKettering_QAMPER_IVIM.py b/src/standardized/ASD_MemorialSloanKettering_QAMPER_IVIM.py index a5a92d49..3b98fc74 100644 --- a/src/standardized/ASD_MemorialSloanKettering_QAMPER_IVIM.py +++ b/src/standardized/ASD_MemorialSloanKettering_QAMPER_IVIM.py @@ -44,17 +44,17 @@ def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=Non #super(OGC_AmsterdamUMC_biexp, self).__init__(bvalues, bounds, initial_guess, fitS0) super(ASD_MemorialSloanKettering_QAMPER_IVIM, self).__init__(bvalues=bvalues, bounds=bounds, initial_guess=initial_guess) self.initialize(bounds, initial_guess) + self.eng=matlab.engine.start_matlab() + def algorithm(self,dwi_arr, bval_arr, LB0, UB0, x0in): - eng = matlab.engine.start_matlab() dwi_arr = matlab.double(dwi_arr.tolist()) bval_arr = matlab.double(bval_arr.tolist()) LB0 = matlab.double(LB0.tolist()) UB0 = matlab.double(UB0.tolist()) x0in = matlab.double(x0in.tolist()) - f_arr, D_arr, Dx_arr, s0_arr, fitted_dwi_arr, RSS, rms_val, chi, AIC, BIC, R_sq = eng.IVIM_standard_bcin( + f_arr, D_arr, Dx_arr, s0_arr, fitted_dwi_arr, RSS, rms_val, chi, AIC, BIC, R_sq = self.eng.IVIM_standard_bcin( dwi_arr, bval_arr, 0.0, LB0, UB0, x0in, False, 0, 0) - eng.quit() return D_arr, f_arr, Dx_arr, s0_arr def initialize(self, bounds, initial_guess): @@ -94,4 +94,24 @@ def ivim_fit(self, signals, bvalues, **kwargs): results["f"] = fit_results[1] results["Dp"] = fit_results[2] - return results \ No newline at end of file + return results + + + def __del__(self): + if hasattr(self, "eng") and self.eng: + try: + self.eng.quit() + except Exception as e: + print(f"Warning: Failed to quit MATLAB engine cleanly: {e}") + + + def __enter__(self): + return self + + + def __exit__(self, exc_type, exc_val, exc_tb): + if hasattr(self, "eng") and self.eng: + try: + self.eng.quit() + except Exception as e: + print(f"Warning: Failed to quit MATLAB engine cleanly: {e}") From 9d407fd6c4076875000c2790ac117e2f66b3a5b0 Mon Sep 17 00:00:00 2001 From: Oliver Gurney-Champion Date: Fri, 30 May 2025 11:01:34 +0200 Subject: [PATCH 03/18] Wrapper now runs for ASD Memorial Sloan Kettering. But unit tests fail --- .../MRI-QAMPER_IVIM/IVIM_standard_bcin.m | 1 - .../ASD_MemorialSloanKettering_QAMPER_IVIM.py | 9 +++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/original/ASD_MemorialSloanKettering/MRI-QAMPER_IVIM/IVIM_standard_bcin.m b/src/original/ASD_MemorialSloanKettering/MRI-QAMPER_IVIM/IVIM_standard_bcin.m index 220980fc..09828228 100755 --- a/src/original/ASD_MemorialSloanKettering/MRI-QAMPER_IVIM/IVIM_standard_bcin.m +++ b/src/original/ASD_MemorialSloanKettering/MRI-QAMPER_IVIM/IVIM_standard_bcin.m @@ -7,7 +7,6 @@ end numVoxels = size(dwi_arr,2); -print(numVoxels) numBvals = length(bval_arr); f_lb = 0; diff --git a/src/standardized/ASD_MemorialSloanKettering_QAMPER_IVIM.py b/src/standardized/ASD_MemorialSloanKettering_QAMPER_IVIM.py index 3b98fc74..0c651922 100644 --- a/src/standardized/ASD_MemorialSloanKettering_QAMPER_IVIM.py +++ b/src/standardized/ASD_MemorialSloanKettering_QAMPER_IVIM.py @@ -53,9 +53,10 @@ def algorithm(self,dwi_arr, bval_arr, LB0, UB0, x0in): LB0 = matlab.double(LB0.tolist()) UB0 = matlab.double(UB0.tolist()) x0in = matlab.double(x0in.tolist()) - f_arr, D_arr, Dx_arr, s0_arr, fitted_dwi_arr, RSS, rms_val, chi, AIC, BIC, R_sq = self.eng.IVIM_standard_bcin( - dwi_arr, bval_arr, 0.0, LB0, UB0, x0in, False, 0, 0) - return D_arr, f_arr, Dx_arr, s0_arr + results = self.eng.IVIM_standard_bcin( + dwi_arr, bval_arr, 0.0, LB0, UB0, x0in, False, 0, 0,nargout=11) + (f_arr, D_arr, Dx_arr, s0_arr, fitted_dwi_arr, RSS, rms_val, chi, AIC, BIC, R_sq) = results + return D_arr/1000, f_arr, Dx_arr/1000, s0_arr def initialize(self, bounds, initial_guess): if bounds is None: @@ -87,7 +88,7 @@ def ivim_fit(self, signals, bvalues, **kwargs): LB = np.array(self.bounds[0]) UB = np.array(self.bounds[1]) - fit_results = self.algorithm(np.array(signals), bvalues, LB, UB, np.array(self.initial_guess)) + fit_results = self.algorithm(np.array(signals)[:,np.newaxis], bvalues, LB, UB, np.array(self.initial_guess)) results = {} results["D"] = fit_results[0] From d471105a6ac8869ad79b4c7bb8715fa8f8376396 Mon Sep 17 00:00:00 2001 From: Oliver Gurney-Champion Date: Mon, 2 Jun 2025 14:34:42 +0200 Subject: [PATCH 04/18] fixed bugs - fixed initial guesses and bounds such that it passes tests - changed matlab huild for github, hopefully overcoming errors. --- .github/workflows/unit_test.yml | 7 ++- .gitignore | 2 +- .../ASD_MemorialSloanKettering_QAMPER_IVIM.py | 43 +++++++++++-------- src/standardized/OGC_AmsterdamUMC_biexp.py | 2 +- tests/IVIMmodels/unit_tests/algorithms.json | 4 ++ tests/IVIMmodels/unit_tests/test_ivim_fit.py | 13 ++++-- 6 files changed, 46 insertions(+), 25 deletions(-) diff --git a/.github/workflows/unit_test.yml b/.github/workflows/unit_test.yml index 946a340b..a9dce575 100644 --- a/.github/workflows/unit_test.yml +++ b/.github/workflows/unit_test.yml @@ -34,8 +34,11 @@ jobs: uses: actions/checkout@v4 - name: Set up MATLAB uses: matlab-actions/setup-matlab@v2 - - name: Run build - uses: matlab-actions/run-build@v2 + with: + release: R2025a + - name: Install MATLAB Engine for Python + run: | + python -m pip install matlabengine==25.1.2 - name: Test with pytest run: | pip install pytest pytest-cov diff --git a/.gitignore b/.gitignore index faf3523d..a4125b0c 100644 --- a/.gitignore +++ b/.gitignore @@ -19,7 +19,7 @@ md5sums.txt *.asv src/original/ASD_MemorialSloanKettering/MRI-QAMPER_IVIM/test_data src/original/ASD_MemorialSloanKettering/MRI-QAMPER_IVIM/output_files - +tests/IVIMmodels/unit_tests/*.log # Unit test / coverage reports diff --git a/src/standardized/ASD_MemorialSloanKettering_QAMPER_IVIM.py b/src/standardized/ASD_MemorialSloanKettering_QAMPER_IVIM.py index 0c651922..887dbbd2 100644 --- a/src/standardized/ASD_MemorialSloanKettering_QAMPER_IVIM.py +++ b/src/standardized/ASD_MemorialSloanKettering_QAMPER_IVIM.py @@ -33,7 +33,7 @@ class ASD_MemorialSloanKettering_QAMPER_IVIM(OsipiBase): supported_initial_guess = True supported_thresholds = False - def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=None): + def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=None,eng=None): """ Everything this algorithm requires should be implemented here. Number of segmentation thresholds, bounds, etc. @@ -44,7 +44,14 @@ def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=Non #super(OGC_AmsterdamUMC_biexp, self).__init__(bvalues, bounds, initial_guess, fitS0) super(ASD_MemorialSloanKettering_QAMPER_IVIM, self).__init__(bvalues=bvalues, bounds=bounds, initial_guess=initial_guess) self.initialize(bounds, initial_guess) - self.eng=matlab.engine.start_matlab() + if eng is None: + print('initiating matlab; this may take some time. For repeated testing one could use the optional input eng as an already initiated matlab engine') + self.eng=matlab.engine.start_matlab() + self.keep_alive=False + else: + self.eng = eng + self.keep_alive=True + def algorithm(self,dwi_arr, bval_arr, LB0, UB0, x0in): @@ -61,12 +68,12 @@ def algorithm(self,dwi_arr, bval_arr, LB0, UB0, x0in): def initialize(self, bounds, initial_guess): if bounds is None: print('warning, no bounds were defined, so algorithm-specific default bounds are used') - self.bounds=([1e-6, 0, 1e-6, 0],[0.003, 1.0, 5e-2, 5]) + self.bounds=([1e-6, 0, 0.004, 0],[0.003, 1.0, 0.2, 5]) else: self.bounds=bounds if initial_guess is None: print('warning, no initial guesses were defined, so algorithm-specific default initial guess is used') - self.initial_guess = [0.001, 0.001, 0.2, 1] + self.initial_guess = [0.001, 0.2, 0.01, 1] else: self.initial_guess = initial_guess self.use_initial_guess = True @@ -85,10 +92,10 @@ def ivim_fit(self, signals, bvalues, **kwargs): """ bvalues=np.array(bvalues) - LB = np.array(self.bounds[0]) - UB = np.array(self.bounds[1]) + LB = np.array(self.bounds[0])[[1,0,2,3]] + UB = np.array(self.bounds[1])[[1,0,2,3]] - fit_results = self.algorithm(np.array(signals)[:,np.newaxis], bvalues, LB, UB, np.array(self.initial_guess)) + fit_results = self.algorithm(np.array(signals)[:,np.newaxis], bvalues, LB, UB, np.array(self.initial_guess)[[1,0,2,3]]) results = {} results["D"] = fit_results[0] @@ -99,11 +106,12 @@ def ivim_fit(self, signals, bvalues, **kwargs): def __del__(self): - if hasattr(self, "eng") and self.eng: - try: - self.eng.quit() - except Exception as e: - print(f"Warning: Failed to quit MATLAB engine cleanly: {e}") + if not self.keep_alive: + if hasattr(self, "eng") and self.eng: + try: + self.eng.quit() + except Exception as e: + print(f"Warning: Failed to quit MATLAB engine cleanly: {e}") def __enter__(self): @@ -111,8 +119,9 @@ def __enter__(self): def __exit__(self, exc_type, exc_val, exc_tb): - if hasattr(self, "eng") and self.eng: - try: - self.eng.quit() - except Exception as e: - print(f"Warning: Failed to quit MATLAB engine cleanly: {e}") + if not self.keep_alive: + if hasattr(self, "eng") and self.eng: + try: + self.eng.quit() + except Exception as e: + print(f"Warning: Failed to quit MATLAB engine cleanly: {e}") diff --git a/src/standardized/OGC_AmsterdamUMC_biexp.py b/src/standardized/OGC_AmsterdamUMC_biexp.py index 49a16b27..5afd0cb8 100644 --- a/src/standardized/OGC_AmsterdamUMC_biexp.py +++ b/src/standardized/OGC_AmsterdamUMC_biexp.py @@ -55,7 +55,7 @@ def initialize(self, bounds, initial_guess, fitS0): self.bounds=bounds if initial_guess is None: print('warning, no initial guesses were defined, so default bounds are used of [0.001, 0.001, 0.01, 1]') - self.initial_guess = [0.001, 0.001, 0.01, 1] + self.initial_guess = [0.001, 0.1, 0.01, 1] else: self.initial_guess = initial_guess self.use_initial_guess = True diff --git a/tests/IVIMmodels/unit_tests/algorithms.json b/tests/IVIMmodels/unit_tests/algorithms.json index c6ca198e..3e289721 100644 --- a/tests/IVIMmodels/unit_tests/algorithms.json +++ b/tests/IVIMmodels/unit_tests/algorithms.json @@ -15,6 +15,10 @@ "PvH_KB_NKI_IVIMfit", "OJ_GU_seg" ], + "ASD_MemorialSloanKettering_QAMPER_IVIM": { + "requieres_matlab": true, + "fail_first_time": true + }, "IAR_LU_biexp": { "fail_first_time": true }, diff --git a/tests/IVIMmodels/unit_tests/test_ivim_fit.py b/tests/IVIMmodels/unit_tests/test_ivim_fit.py index fb58d13b..5711d879 100644 --- a/tests/IVIMmodels/unit_tests/test_ivim_fit.py +++ b/tests/IVIMmodels/unit_tests/test_ivim_fit.py @@ -6,6 +6,9 @@ import time from src.wrappers.OsipiBase import OsipiBase #run using python -m pytest from the root folder +import matlab.engine + +eng=matlab.engine.start_matlab() def signal_helper(signal): signal = np.asarray(signal) @@ -44,9 +47,9 @@ def data_ivim_fit_saved(): algorithms = algorithm_information["algorithms"] bvals = all_data.pop('config') bvals = bvals['bvalues'] - first=True - for name, data in all_data.items(): - for algorithm in algorithms: + for algorithm in algorithms: + first = True + for name, data in all_data.items(): algorithm_dict = algorithm_information.get(algorithm, {}) xfail = {"xfail": name in algorithm_dict.get("xfail_names", {}), "strict": algorithm_dict.get("xfail_names", {}).get(name, True)} @@ -56,7 +59,9 @@ def data_ivim_fit_saved(): if first == True: if algorithm_dict.get("fail_first_time", {}) == True: skiptime = True - first = False + first = False # this will only work for 1 algorithm... If two algorithms use fail_first_time, the second will not see it is the first time. + if algorithm_dict.get("requieres_matlab", {}) == True: + kwargs={**kwargs,'eng': eng} yield name, bvals, data, algorithm, xfail, kwargs, tolerances, skiptime @pytest.mark.parametrize("name, bvals, data, algorithm, xfail, kwargs, tolerances, skiptime", data_ivim_fit_saved()) From 648a7a6ab93dd863b8c51618ad986aafc57dd3b2 Mon Sep 17 00:00:00 2001 From: Oliver Gurney-Champion Date: Mon, 2 Jun 2025 15:07:31 +0200 Subject: [PATCH 05/18] fix bug for linux and mac --- .github/workflows/unit_test.yml | 10 ++++++++++ tests/IVIMmodels/unit_tests/test_ivim_fit.py | 16 +++++++++++----- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/.github/workflows/unit_test.yml b/.github/workflows/unit_test.yml index a9dce575..fef5dab1 100644 --- a/.github/workflows/unit_test.yml +++ b/.github/workflows/unit_test.yml @@ -32,6 +32,15 @@ jobs: pip install -r requirements.txt - name: Check out repository uses: actions/checkout@v4 + - name: Set environment variables for MATLAB Engine + run: | + if [[ "${{ runner.os }}" == "macOS" ]]; then + echo "DYLD_LIBRARY_PATH=/Applications/MATLAB_R2025a.app/bin/maci64:$DYLD_LIBRARY_PATH" >> $GITHUB_ENV + elif [[ "${{ runner.os }}" == "Linux" ]]; then + echo "LD_LIBRARY_PATH=/usr/local/MATLAB/R2025a/bin/glnxa64:$LD_LIBRARY_PATH" >> $GITHUB_ENV + elif [[ "${{ runner.os }}" == "Windows" ]]; then + echo "C:\Program Files\MATLAB\R2025a\bin\win64" >> $GITHUB_PATH + fi - name: Set up MATLAB uses: matlab-actions/setup-matlab@v2 with: @@ -39,6 +48,7 @@ jobs: - name: Install MATLAB Engine for Python run: | python -m pip install matlabengine==25.1.2 + - name: Test with pytest run: | pip install pytest pytest-cov diff --git a/tests/IVIMmodels/unit_tests/test_ivim_fit.py b/tests/IVIMmodels/unit_tests/test_ivim_fit.py index 5711d879..f00e68af 100644 --- a/tests/IVIMmodels/unit_tests/test_ivim_fit.py +++ b/tests/IVIMmodels/unit_tests/test_ivim_fit.py @@ -108,11 +108,15 @@ def algorithms(): algorithm_information = json.load(f) algorithms = algorithm_information["algorithms"] for algorithm in algorithms: - yield algorithm - -@pytest.mark.parametrize("algorithm", algorithms()) -def test_default_bounds_and_initial_guesses(algorithm): - fit = OsipiBase(algorithm=algorithm) + algorithm_dict = algorithm_information.get(algorithm, {}) + args={} + if algorithm_dict.get("requieres_matlab", {}) == True: + args['eng'] = eng + yield algorithm, args + +@pytest.mark.parametrize("algorithm, args", algorithms()) +def test_default_bounds_and_initial_guesses(algorithm, args): + fit = OsipiBase(algorithm=algorithm,**args) #assert fit.bounds is not None, f"For {algorithm}, there is no default fit boundary" #assert fit.initial_guess is not None, f"For {algorithm}, there is no default fit initial guess" if fit.use_bounds: @@ -154,6 +158,8 @@ def bound_input(): "strict": algorithm_dict.get("xfail_names", {}).get(name, True)} kwargs = algorithm_dict.get("options", {}) tolerances = algorithm_dict.get("tolerances", {}) + if algorithm_dict.get("requieres_matlab", {}) == True: + kwargs={**kwargs,'eng': eng} yield name, bvals, data, algorithm, xfail, kwargs, tolerances From 8ffbeb4a14ffe58fc13d6e15a0c1fe1a4f65498e Mon Sep 17 00:00:00 2001 From: Oliver Gurney-Champion Date: Mon, 2 Jun 2025 15:23:52 +0200 Subject: [PATCH 06/18] update workflow... --- .github/workflows/unit_test.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/unit_test.yml b/.github/workflows/unit_test.yml index fef5dab1..06aa4c90 100644 --- a/.github/workflows/unit_test.yml +++ b/.github/workflows/unit_test.yml @@ -35,11 +35,9 @@ jobs: - name: Set environment variables for MATLAB Engine run: | if [[ "${{ runner.os }}" == "macOS" ]]; then - echo "DYLD_LIBRARY_PATH=/Applications/MATLAB_R2025a.app/bin/maci64:$DYLD_LIBRARY_PATH" >> $GITHUB_ENV + echo "DYLD_LIBRARY_PATH=/Users/runner/hostedtoolcache/MATLAB/2025.1.999/arm64/MATLAB.app/bin/maca64:$DYLD_LIBRARY_PATH" >> $GITHUB_ENV elif [[ "${{ runner.os }}" == "Linux" ]]; then - echo "LD_LIBRARY_PATH=/usr/local/MATLAB/R2025a/bin/glnxa64:$LD_LIBRARY_PATH" >> $GITHUB_ENV - elif [[ "${{ runner.os }}" == "Windows" ]]; then - echo "C:\Program Files\MATLAB\R2025a\bin\win64" >> $GITHUB_PATH + echo "LD_LIBRARY_PATH=/opt/hostedtoolcache/MATLAB/2025.1.999/x64/bin/glnxa64:$LD_LIBRARY_PATH" >> $GITHUB_ENV fi - name: Set up MATLAB uses: matlab-actions/setup-matlab@v2 From 1159bc2cfaca8aa5a81ffc0029b91070a3de2eec Mon Sep 17 00:00:00 2001 From: Oliver Gurney-Champion Date: Mon, 2 Jun 2025 17:11:01 +0200 Subject: [PATCH 07/18] Update unit_test.yml --- .github/workflows/unit_test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/unit_test.yml b/.github/workflows/unit_test.yml index 06aa4c90..3517c2ba 100644 --- a/.github/workflows/unit_test.yml +++ b/.github/workflows/unit_test.yml @@ -33,6 +33,7 @@ jobs: - name: Check out repository uses: actions/checkout@v4 - name: Set environment variables for MATLAB Engine + if: ${{ runner.os == 'Linux' || runner.os == 'macOS' }} run: | if [[ "${{ runner.os }}" == "macOS" ]]; then echo "DYLD_LIBRARY_PATH=/Users/runner/hostedtoolcache/MATLAB/2025.1.999/arm64/MATLAB.app/bin/maca64:$DYLD_LIBRARY_PATH" >> $GITHUB_ENV From c2e9bd2108a727bbb46f7de2de35d8a82a228184 Mon Sep 17 00:00:00 2001 From: Oliver Gurney-Champion Date: Tue, 3 Jun 2025 12:01:28 +0200 Subject: [PATCH 08/18] update unit test --- .github/workflows/unit_test.yml | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/workflows/unit_test.yml b/.github/workflows/unit_test.yml index 3517c2ba..bb407502 100644 --- a/.github/workflows/unit_test.yml +++ b/.github/workflows/unit_test.yml @@ -30,16 +30,13 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements.txt - - name: Check out repository - uses: actions/checkout@v4 - name: Set environment variables for MATLAB Engine - if: ${{ runner.os == 'Linux' || runner.os == 'macOS' }} + if: ${{ runner.os == 'macOS'}} + run: | + echo "DYLD_LIBRARY_PATH=/Users/runner/hostedtoolcache/MATLAB/2025.1.999/arm64/MATLAB.app/bin/maca64:$DYLD_LIBRARY_PATH" >> $GITHUB_ENV + if: ${{ runner.os == 'Linux'}} run: | - if [[ "${{ runner.os }}" == "macOS" ]]; then - echo "DYLD_LIBRARY_PATH=/Users/runner/hostedtoolcache/MATLAB/2025.1.999/arm64/MATLAB.app/bin/maca64:$DYLD_LIBRARY_PATH" >> $GITHUB_ENV - elif [[ "${{ runner.os }}" == "Linux" ]]; then echo "LD_LIBRARY_PATH=/opt/hostedtoolcache/MATLAB/2025.1.999/x64/bin/glnxa64:$LD_LIBRARY_PATH" >> $GITHUB_ENV - fi - name: Set up MATLAB uses: matlab-actions/setup-matlab@v2 with: From aa3c5b09b9b217aee5df3b5acb1a544a941e009b Mon Sep 17 00:00:00 2001 From: Oliver Gurney-Champion Date: Tue, 3 Jun 2025 12:09:09 +0200 Subject: [PATCH 09/18] fix? --- .github/workflows/unit_test.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/unit_test.yml b/.github/workflows/unit_test.yml index bb407502..c2766a09 100644 --- a/.github/workflows/unit_test.yml +++ b/.github/workflows/unit_test.yml @@ -1,7 +1,5 @@ name: Unit tests - on: [push, pull_request] - jobs: build: @@ -36,7 +34,7 @@ jobs: echo "DYLD_LIBRARY_PATH=/Users/runner/hostedtoolcache/MATLAB/2025.1.999/arm64/MATLAB.app/bin/maca64:$DYLD_LIBRARY_PATH" >> $GITHUB_ENV if: ${{ runner.os == 'Linux'}} run: | - echo "LD_LIBRARY_PATH=/opt/hostedtoolcache/MATLAB/2025.1.999/x64/bin/glnxa64:$LD_LIBRARY_PATH" >> $GITHUB_ENV + echo "LD_LIBRARY_PATH=/opt/hostedtoolcache/MATLAB/2025.1.999/x64/bin/glnxa64:$LD_LIBRARY_PATH" >> $GITHUB_ENV - name: Set up MATLAB uses: matlab-actions/setup-matlab@v2 with: @@ -44,7 +42,6 @@ jobs: - name: Install MATLAB Engine for Python run: | python -m pip install matlabengine==25.1.2 - - name: Test with pytest run: | pip install pytest pytest-cov From 4e2fd54f4bb2147a1fc01eeda25817497eaa690e Mon Sep 17 00:00:00 2001 From: Oliver Gurney-Champion Date: Tue, 3 Jun 2025 12:16:02 +0200 Subject: [PATCH 10/18] Update unit_test.yml --- .github/workflows/unit_test.yml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/unit_test.yml b/.github/workflows/unit_test.yml index c2766a09..ca2f15b4 100644 --- a/.github/workflows/unit_test.yml +++ b/.github/workflows/unit_test.yml @@ -28,13 +28,12 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements.txt - - name: Set environment variables for MATLAB Engine - if: ${{ runner.os == 'macOS'}} - run: | - echo "DYLD_LIBRARY_PATH=/Users/runner/hostedtoolcache/MATLAB/2025.1.999/arm64/MATLAB.app/bin/maca64:$DYLD_LIBRARY_PATH" >> $GITHUB_ENV - if: ${{ runner.os == 'Linux'}} - run: | - echo "LD_LIBRARY_PATH=/opt/hostedtoolcache/MATLAB/2025.1.999/x64/bin/glnxa64:$LD_LIBRARY_PATH" >> $GITHUB_ENV + - name: Set env var for MATLAB on macOS + if: runner.os == 'macOS' + run: echo "DYLD_LIBRARY_PATH=/Users/runner/hostedtoolcache/MATLAB/2025.1.999/arm64/MATLAB.app/bin/maca64:$DYLD_LIBRARY_PATH" >> $GITHUB_ENV + - name: Set env var for MATLAB on Linux + if: runner.os == 'Linux' + run: echo "LD_LIBRARY_PATH=/opt/hostedtoolcache/MATLAB/2025.1.999/x64/bin/glnxa64:$LD_LIBRARY_PATH" >> $GITHUB_ENV - name: Set up MATLAB uses: matlab-actions/setup-matlab@v2 with: From 799920b9e39bb9db3a91fe3bed4eaee21ef0ec0c Mon Sep 17 00:00:00 2001 From: Oliver Gurney-Champion Date: Tue, 3 Jun 2025 12:22:32 +0200 Subject: [PATCH 11/18] Update unit_test.yml --- .github/workflows/unit_test.yml | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/.github/workflows/unit_test.yml b/.github/workflows/unit_test.yml index ca2f15b4..03a2998c 100644 --- a/.github/workflows/unit_test.yml +++ b/.github/workflows/unit_test.yml @@ -1,8 +1,8 @@ name: Unit tests on: [push, pull_request] + jobs: build: - runs-on: ${{ matrix.os }} continue-on-error: false strategy: @@ -13,34 +13,40 @@ jobs: exclude: - os: windows-latest python-version: "3.13" - # Windows chokes on py3.13: https://github.com/numpy/numpy/issues/27894 + steps: - uses: actions/checkout@v4 + - name: Set up Python uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: 'pip' - # You can test your matrix by printing the current Python version + - name: Display Python version run: python -c "import sys; print(sys.version)" + - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt - - name: Set env var for MATLAB on macOS - if: runner.os == 'macOS' - run: echo "DYLD_LIBRARY_PATH=/Users/runner/hostedtoolcache/MATLAB/2025.1.999/arm64/MATLAB.app/bin/maca64:$DYLD_LIBRARY_PATH" >> $GITHUB_ENV - - name: Set env var for MATLAB on Linux - if: runner.os == 'Linux' - run: echo "LD_LIBRARY_PATH=/opt/hostedtoolcache/MATLAB/2025.1.999/x64/bin/glnxa64:$LD_LIBRARY_PATH" >> $GITHUB_ENV + + - name: Set env var for MATLAB on macOS + if: runner.os == 'macOS' + run: echo "DYLD_LIBRARY_PATH=/Users/runner/hostedtoolcache/MATLAB/2025.1.999/arm64/MATLAB.app/bin/maca64:$DYLD_LIBRARY_PATH" >> $GITHUB_ENV + + - name: Set env var for MATLAB on Linux + if: runner.os == 'Linux' + run: echo "LD_LIBRARY_PATH=/opt/hostedtoolcache/MATLAB/2025.1.999/x64/bin/glnxa64:$LD_LIBRARY_PATH" >> $GITHUB_ENV + - name: Set up MATLAB uses: matlab-actions/setup-matlab@v2 with: release: R2025a + - name: Install MATLAB Engine for Python - run: | - python -m pip install matlabengine==25.1.2 + run: python -m pip install matlabengine==25.1.2 + - name: Test with pytest run: | pip install pytest pytest-cov From e75897428fca3415fd645017f768accd457ee1f0 Mon Sep 17 00:00:00 2001 From: Oliver Gurney-Champion Date: Wed, 4 Jun 2025 14:36:56 +0200 Subject: [PATCH 12/18] add flag for running matlab tests --- tests/IVIMmodels/unit_tests/algorithms.json | 1 - tests/IVIMmodels/unit_tests/test_ivim_fit.py | 36 ++++++++++++++++---- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/tests/IVIMmodels/unit_tests/algorithms.json b/tests/IVIMmodels/unit_tests/algorithms.json index 3e289721..1f64576f 100644 --- a/tests/IVIMmodels/unit_tests/algorithms.json +++ b/tests/IVIMmodels/unit_tests/algorithms.json @@ -17,7 +17,6 @@ ], "ASD_MemorialSloanKettering_QAMPER_IVIM": { "requieres_matlab": true, - "fail_first_time": true }, "IAR_LU_biexp": { "fail_first_time": true diff --git a/tests/IVIMmodels/unit_tests/test_ivim_fit.py b/tests/IVIMmodels/unit_tests/test_ivim_fit.py index f00e68af..d2b3a147 100644 --- a/tests/IVIMmodels/unit_tests/test_ivim_fit.py +++ b/tests/IVIMmodels/unit_tests/test_ivim_fit.py @@ -6,9 +6,22 @@ import time from src.wrappers.OsipiBase import OsipiBase #run using python -m pytest from the root folder -import matlab.engine -eng=matlab.engine.start_matlab() + +eng = None + +def pytest_addoption(parser): + parser.addoption( + "--with-matlab", action="store_true", default=False, help="Run MATLAB-dependent tests" + ) + +@pytest.hookimpl +def pytest_configure(config): + global eng + if config.getoption("--with-matlab"): + import matlab.engine + eng = matlab.engine.start_matlab() + def signal_helper(signal): signal = np.asarray(signal) @@ -61,7 +74,10 @@ def data_ivim_fit_saved(): skiptime = True first = False # this will only work for 1 algorithm... If two algorithms use fail_first_time, the second will not see it is the first time. if algorithm_dict.get("requieres_matlab", {}) == True: - kwargs={**kwargs,'eng': eng} + if eng is None: + continue + else: + kwargs={**kwargs,'eng': eng} yield name, bvals, data, algorithm, xfail, kwargs, tolerances, skiptime @pytest.mark.parametrize("name, bvals, data, algorithm, xfail, kwargs, tolerances, skiptime", data_ivim_fit_saved()) @@ -100,7 +116,7 @@ def to_list_if_needed(value): assert elapsed_time < 0.5, f"Algorithm {name} took {elapsed_time} seconds, which is longer than 2 second to fit per voxel" #less than 0.5 seconds per voxel -def algorithms(): +def algorithmlist(): # Find the algorithms from algorithms.json file = pathlib.Path(__file__) algorithm_path = file.with_name('algorithms.json') @@ -111,10 +127,13 @@ def algorithms(): algorithm_dict = algorithm_information.get(algorithm, {}) args={} if algorithm_dict.get("requieres_matlab", {}) == True: - args['eng'] = eng + if eng is None: + continue + else: + kwargs = {**kwargs, 'eng': eng} yield algorithm, args -@pytest.mark.parametrize("algorithm, args", algorithms()) +@pytest.mark.parametrize("algorithm, args", algorithmlist()) def test_default_bounds_and_initial_guesses(algorithm, args): fit = OsipiBase(algorithm=algorithm,**args) #assert fit.bounds is not None, f"For {algorithm}, there is no default fit boundary" @@ -159,7 +178,10 @@ def bound_input(): kwargs = algorithm_dict.get("options", {}) tolerances = algorithm_dict.get("tolerances", {}) if algorithm_dict.get("requieres_matlab", {}) == True: - kwargs={**kwargs,'eng': eng} + if eng is None: + continue + else: + kwargs = {**kwargs, 'eng': eng} yield name, bvals, data, algorithm, xfail, kwargs, tolerances From fc31bc6d0a4bc91d69c0cffb532b7d56bffdff89 Mon Sep 17 00:00:00 2001 From: Oliver Gurney-Champion Date: Wed, 4 Jun 2025 14:43:30 +0200 Subject: [PATCH 13/18] remove matlab install from workflow --- .github/workflows/unit_test.yml | 25 ++++--------------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/.github/workflows/unit_test.yml b/.github/workflows/unit_test.yml index 03a2998c..c5475ed2 100644 --- a/.github/workflows/unit_test.yml +++ b/.github/workflows/unit_test.yml @@ -1,8 +1,10 @@ name: Unit tests + on: [push, pull_request] jobs: build: + runs-on: ${{ matrix.os }} continue-on-error: false strategy: @@ -13,40 +15,21 @@ jobs: exclude: - os: windows-latest python-version: "3.13" - + # Windows chokes on py3.13: https://github.com/numpy/numpy/issues/27894 steps: - uses: actions/checkout@v4 - - name: Set up Python uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: 'pip' - + # You can test your matrix by printing the current Python version - name: Display Python version run: python -c "import sys; print(sys.version)" - - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt - - - name: Set env var for MATLAB on macOS - if: runner.os == 'macOS' - run: echo "DYLD_LIBRARY_PATH=/Users/runner/hostedtoolcache/MATLAB/2025.1.999/arm64/MATLAB.app/bin/maca64:$DYLD_LIBRARY_PATH" >> $GITHUB_ENV - - - name: Set env var for MATLAB on Linux - if: runner.os == 'Linux' - run: echo "LD_LIBRARY_PATH=/opt/hostedtoolcache/MATLAB/2025.1.999/x64/bin/glnxa64:$LD_LIBRARY_PATH" >> $GITHUB_ENV - - - name: Set up MATLAB - uses: matlab-actions/setup-matlab@v2 - with: - release: R2025a - - - name: Install MATLAB Engine for Python - run: python -m pip install matlabengine==25.1.2 - - name: Test with pytest run: | pip install pytest pytest-cov From 4e4a6dc05e59f55bf1d8a3ad801a849e5d61258e Mon Sep 17 00:00:00 2001 From: Oliver Gurney-Champion Date: Wed, 4 Jun 2025 15:24:00 +0200 Subject: [PATCH 14/18] fix right location for matlab avail --- conftest.py | 17 ++++++++++++++++- tests/IVIMmodels/unit_tests/algorithms.json | 2 +- tests/IVIMmodels/unit_tests/test_ivim_fit.py | 20 +++----------------- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/conftest.py b/conftest.py index 086585c2..bd24d7ef 100644 --- a/conftest.py +++ b/conftest.py @@ -4,7 +4,6 @@ import csv # import datetime - def pytest_addoption(parser): parser.addoption( "--SNR", @@ -81,6 +80,22 @@ def pytest_addoption(parser): type=str, help="Drop this algorithm from the list" ) + parser.addoption( + "--with-matlab", + action="store_true", + default=False, + help="Run MATLAB-dependent tests" + ) + +eng = None + +def pytest_configure(config): + global eng + if config.getoption("--with-matlab"): + import matlab.engine + print("Starting MATLAB engine...") + eng = matlab.engine.start_matlab() + print("MATLAB engine started.") @pytest.fixture(scope="session") diff --git a/tests/IVIMmodels/unit_tests/algorithms.json b/tests/IVIMmodels/unit_tests/algorithms.json index 1f64576f..584d55c4 100644 --- a/tests/IVIMmodels/unit_tests/algorithms.json +++ b/tests/IVIMmodels/unit_tests/algorithms.json @@ -16,7 +16,7 @@ "OJ_GU_seg" ], "ASD_MemorialSloanKettering_QAMPER_IVIM": { - "requieres_matlab": true, + "requieres_matlab": true }, "IAR_LU_biexp": { "fail_first_time": true diff --git a/tests/IVIMmodels/unit_tests/test_ivim_fit.py b/tests/IVIMmodels/unit_tests/test_ivim_fit.py index d2b3a147..898d51b8 100644 --- a/tests/IVIMmodels/unit_tests/test_ivim_fit.py +++ b/tests/IVIMmodels/unit_tests/test_ivim_fit.py @@ -5,24 +5,10 @@ import pathlib import time from src.wrappers.OsipiBase import OsipiBase +from conftest import eng #run using python -m pytest from the root folder -eng = None - -def pytest_addoption(parser): - parser.addoption( - "--with-matlab", action="store_true", default=False, help="Run MATLAB-dependent tests" - ) - -@pytest.hookimpl -def pytest_configure(config): - global eng - if config.getoption("--with-matlab"): - import matlab.engine - eng = matlab.engine.start_matlab() - - def signal_helper(signal): signal = np.asarray(signal) #signal[signal < 0] = 0.00001 @@ -72,7 +58,7 @@ def data_ivim_fit_saved(): if first == True: if algorithm_dict.get("fail_first_time", {}) == True: skiptime = True - first = False # this will only work for 1 algorithm... If two algorithms use fail_first_time, the second will not see it is the first time. + first = False if algorithm_dict.get("requieres_matlab", {}) == True: if eng is None: continue @@ -130,7 +116,7 @@ def algorithmlist(): if eng is None: continue else: - kwargs = {**kwargs, 'eng': eng} + args = {**args, 'eng': eng} yield algorithm, args @pytest.mark.parametrize("algorithm, args", algorithmlist()) From 52d7debc20b6ce475ec3830d2a1f2425fa27918d Mon Sep 17 00:00:00 2001 From: Oliver Gurney-Champion Date: Wed, 4 Jun 2025 15:57:21 +0200 Subject: [PATCH 15/18] exclude matlab algorithms from testing algorithm list if matlab is not isntalled --- conftest.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/conftest.py b/conftest.py index bd24d7ef..9d052624 100644 --- a/conftest.py +++ b/conftest.py @@ -81,7 +81,7 @@ def pytest_addoption(parser): help="Drop this algorithm from the list" ) parser.addoption( - "--with-matlab", + "--withmatlab", action="store_true", default=False, help="Run MATLAB-dependent tests" @@ -91,7 +91,7 @@ def pytest_addoption(parser): def pytest_configure(config): global eng - if config.getoption("--with-matlab"): + if config.getoption("--withmatlab"): import matlab.engine print("Starting MATLAB engine...") eng = matlab.engine.start_matlab() @@ -178,6 +178,11 @@ def algorithm_list(filename, selected, dropped): with algorithm_path.open() as f: algorithm_information = json.load(f) algorithms = set(algorithm_information["algorithms"]) + for algorithm in algorithms: + algorithm_dict = algorithm_information.get(algorithm, {}) + if algorithm_dict.get("requieres_matlab", {}) == True: + if eng is None: + algorithms = algorithms - set(algorithm) algorithms = algorithms - set(dropped) if len(selected) > 0 and selected[0]: algorithms = algorithms & set(selected) From b7f2f8ea7d8611a5df67605531c0f4de34699542 Mon Sep 17 00:00:00 2001 From: Oliver Gurney-Champion Date: Wed, 4 Jun 2025 16:29:42 +0200 Subject: [PATCH 16/18] ensure testing is only done in the test folder when testing in all folders, all code is initiated. When matlab is not available, it gives an error. --- .gitignore | 4 +++- pytest.ini | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index a4125b0c..977ba281 100644 --- a/.gitignore +++ b/.gitignore @@ -20,7 +20,9 @@ md5sums.txt src/original/ASD_MemorialSloanKettering/MRI-QAMPER_IVIM/test_data src/original/ASD_MemorialSloanKettering/MRI-QAMPER_IVIM/output_files tests/IVIMmodels/unit_tests/*.log - +junit/* +ivim_simulation.bval +ivim_simulation.bvec # Unit test / coverage reports .tox/ diff --git a/pytest.ini b/pytest.ini index e61feaf8..ed02be7a 100644 --- a/pytest.ini +++ b/pytest.ini @@ -2,4 +2,5 @@ markers = slow: marks tests as slow (deselect with '-m "not slow"') addopts = - -m 'not slow' \ No newline at end of file + -m 'not slow' +testpaths = tests \ No newline at end of file From c32bfbdceb6972d3639b96da58302e904e313838 Mon Sep 17 00:00:00 2001 From: Oliver Gurney-Champion Date: Tue, 10 Jun 2025 21:20:01 +0200 Subject: [PATCH 17/18] Fixed comments - Moved all the initiation functions from the unit_test folder to conftest.py - Added a skip option for all matlab tests (instead of removing them) - Removed some typos - Removed "redundant" code --- conftest.py | 120 +++++++++++++---- .../ASD_MemorialSloanKettering_QAMPER_IVIM.py | 19 +-- tests/IVIMmodels/unit_tests/algorithms.json | 2 +- tests/IVIMmodels/unit_tests/test_ivim_fit.py | 125 +++++------------- .../unit_tests/test_ivim_synthetic.py | 6 +- 5 files changed, 137 insertions(+), 135 deletions(-) diff --git a/conftest.py b/conftest.py index 9d052624..598820e2 100644 --- a/conftest.py +++ b/conftest.py @@ -4,6 +4,7 @@ import csv # import datetime + def pytest_addoption(parser): parser.addoption( "--SNR", @@ -87,15 +88,17 @@ def pytest_addoption(parser): help="Run MATLAB-dependent tests" ) -eng = None -def pytest_configure(config): - global eng - if config.getoption("--withmatlab"): - import matlab.engine - print("Starting MATLAB engine...") - eng = matlab.engine.start_matlab() - print("MATLAB engine started.") +@pytest.fixture(scope="session") +def eng(request): + """Start and return a MATLAB engine session if --withmatlab is set.""" + if not request.config.getoption("--withmatlab"): + return None + import matlab.engine + print("Starting MATLAB engine...") + eng = matlab.engine.start_matlab() + print("MATLAB engine started.") + return eng @pytest.fixture(scope="session") @@ -164,37 +167,102 @@ def use_prior(request): def pytest_generate_tests(metafunc): if "SNR" in metafunc.fixturenames: metafunc.parametrize("SNR", metafunc.config.getoption("SNR")) - if "ivim_algorithm" in metafunc.fixturenames: - algorithms = algorithm_list(metafunc.config.getoption("algorithmFile"), metafunc.config.getoption("selectAlgorithm"), metafunc.config.getoption("dropAlgorithm")) - metafunc.parametrize("ivim_algorithm", algorithms) if "ivim_data" in metafunc.fixturenames: data = data_list(metafunc.config.getoption("dataFile")) metafunc.parametrize("ivim_data", data) + if "data_ivim_fit_saved" in metafunc.fixturenames: + args = data_ivim_fit_saved(metafunc.config.getoption("dataFile"),metafunc.config.getoption("algorithmFile")) + metafunc.parametrize("data_ivim_fit_saved", args) + if "algorithmlist" in metafunc.fixturenames: + args = algorithmlist(metafunc.config.getoption("algorithmFile")) + metafunc.parametrize("algorithmlist", args) + if "bound_input" in metafunc.fixturenames: + args = bound_input(metafunc.config.getoption("dataFile"),metafunc.config.getoption("algorithmFile")) + metafunc.parametrize("bound_input", args) + + +def data_list(filename): + current_folder = pathlib.Path.cwd() + data_path = current_folder / filename + with data_path.open() as f: + all_data = json.load(f) + + bvals = all_data.pop('config') + bvals = bvals['bvalues'] + for name, data in all_data.items(): + yield name, bvals, data + +def data_ivim_fit_saved(datafile, algorithmFile): + # Find the algorithms from algorithms.json + current_folder = pathlib.Path.cwd() + algorithm_path = current_folder / algorithmFile + with algorithm_path.open() as f: + algorithm_information = json.load(f) + # Load generic test data generated from the included phantom: phantoms/MR_XCAT_qMRI + generic = current_folder / datafile + with generic.open() as f: + all_data = json.load(f) + algorithms = algorithm_information["algorithms"] + bvals = all_data.pop('config') + bvals = bvals['bvalues'] + for algorithm in algorithms: + first = True + for name, data in all_data.items(): + algorithm_dict = algorithm_information.get(algorithm, {}) + xfail = {"xfail": name in algorithm_dict.get("xfail_names", {}), + "strict": algorithm_dict.get("xfail_names", {}).get(name, True)} + kwargs = algorithm_dict.get("options", {}) + tolerances = algorithm_dict.get("tolerances", {}) + skiptime=False + if first == True: + if algorithm_dict.get("fail_first_time", {}) == True: + skiptime = True + first = False + if algorithm_dict.get("requires_matlab", False) == True: + requires_matlab = True + else: + requires_matlab = False + yield name, bvals, data, algorithm, xfail, kwargs, tolerances, skiptime, requires_matlab -def algorithm_list(filename, selected, dropped): +def algorithmlist(algorithmFile): + # Find the algorithms from algorithms.json current_folder = pathlib.Path.cwd() - algorithm_path = current_folder / filename + algorithm_path = current_folder / algorithmFile with algorithm_path.open() as f: algorithm_information = json.load(f) - algorithms = set(algorithm_information["algorithms"]) + + algorithms = algorithm_information["algorithms"] for algorithm in algorithms: algorithm_dict = algorithm_information.get(algorithm, {}) - if algorithm_dict.get("requieres_matlab", {}) == True: - if eng is None: - algorithms = algorithms - set(algorithm) - algorithms = algorithms - set(dropped) - if len(selected) > 0 and selected[0]: - algorithms = algorithms & set(selected) - return list(algorithms) + if algorithm_dict.get("requires_matlab", False) == True: + requires_matlab = True + else: + requires_matlab = False + yield algorithm, requires_matlab -def data_list(filename): +def bound_input(datafile,algorithmFile): + # Find the algorithms from algorithms.json current_folder = pathlib.Path.cwd() - data_path = current_folder / filename - with data_path.open() as f: + algorithm_path = current_folder / algorithmFile + with algorithm_path.open() as f: + algorithm_information = json.load(f) + # Load generic test data generated from the included phantom: phantoms/MR_XCAT_qMRI + generic = current_folder / datafile + with generic.open() as f: all_data = json.load(f) - + algorithms = algorithm_information["algorithms"] bvals = all_data.pop('config') bvals = bvals['bvalues'] for name, data in all_data.items(): - yield name, bvals, data + for algorithm in algorithms: + algorithm_dict = algorithm_information.get(algorithm, {}) + xfail = {"xfail": name in algorithm_dict.get("xfail_names", {}), + "strict": algorithm_dict.get("xfail_names", {}).get(name, True)} + kwargs = algorithm_dict.get("options", {}) + tolerances = algorithm_dict.get("tolerances", {}) + if algorithm_dict.get("requires_matlab", False) == True: + requires_matlab = True + else: + requires_matlab = False + yield name, bvals, data, algorithm, xfail, kwargs, tolerances, requires_matlab \ No newline at end of file diff --git a/src/standardized/ASD_MemorialSloanKettering_QAMPER_IVIM.py b/src/standardized/ASD_MemorialSloanKettering_QAMPER_IVIM.py index 887dbbd2..455e6397 100644 --- a/src/standardized/ASD_MemorialSloanKettering_QAMPER_IVIM.py +++ b/src/standardized/ASD_MemorialSloanKettering_QAMPER_IVIM.py @@ -2,6 +2,7 @@ import numpy as np import matlab.engine + class ASD_MemorialSloanKettering_QAMPER_IVIM(OsipiBase): """ Bi-exponential fitting algorithm by Oliver Gurney-Champion, Amsterdam UMC @@ -27,13 +28,12 @@ class ASD_MemorialSloanKettering_QAMPER_IVIM(OsipiBase): 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,eng=None): + def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=None, eng=None): """ Everything this algorithm requires should be implemented here. Number of segmentation thresholds, bounds, etc. @@ -52,8 +52,6 @@ def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=Non self.eng = eng self.keep_alive=True - - def algorithm(self,dwi_arr, bval_arr, LB0, UB0, x0in): dwi_arr = matlab.double(dwi_arr.tolist()) bval_arr = matlab.double(bval_arr.tolist()) @@ -104,8 +102,7 @@ def ivim_fit(self, signals, bvalues, **kwargs): return results - - def __del__(self): + def clean(self): if not self.keep_alive: if hasattr(self, "eng") and self.eng: try: @@ -113,15 +110,11 @@ def __del__(self): except Exception as e: print(f"Warning: Failed to quit MATLAB engine cleanly: {e}") + def __del__(self): + self.clean() def __enter__(self): return self - def __exit__(self, exc_type, exc_val, exc_tb): - if not self.keep_alive: - if hasattr(self, "eng") and self.eng: - try: - self.eng.quit() - except Exception as e: - print(f"Warning: Failed to quit MATLAB engine cleanly: {e}") + self.clean() diff --git a/tests/IVIMmodels/unit_tests/algorithms.json b/tests/IVIMmodels/unit_tests/algorithms.json index 584d55c4..fa90ce5a 100644 --- a/tests/IVIMmodels/unit_tests/algorithms.json +++ b/tests/IVIMmodels/unit_tests/algorithms.json @@ -16,7 +16,7 @@ "OJ_GU_seg" ], "ASD_MemorialSloanKettering_QAMPER_IVIM": { - "requieres_matlab": true + "requires_matlab": true }, "IAR_LU_biexp": { "fail_first_time": true diff --git a/tests/IVIMmodels/unit_tests/test_ivim_fit.py b/tests/IVIMmodels/unit_tests/test_ivim_fit.py index 898d51b8..350d85af 100644 --- a/tests/IVIMmodels/unit_tests/test_ivim_fit.py +++ b/tests/IVIMmodels/unit_tests/test_ivim_fit.py @@ -5,7 +5,6 @@ import pathlib import time from src.wrappers.OsipiBase import OsipiBase -from conftest import eng #run using python -m pytest from the root folder @@ -31,50 +30,24 @@ def tolerances_helper(tolerances, data): tolerances["atol"] = tolerances.get("atol", {"f": 2e-1, "D": 5e-4, "Dp": 4e-2}) return tolerances -def data_ivim_fit_saved(): - # Find the algorithms from algorithms.json - file = pathlib.Path(__file__) - algorithm_path = file.with_name('algorithms.json') - with algorithm_path.open() as f: - algorithm_information = json.load(f) - - # Load generic test data generated from the included phantom: phantoms/MR_XCAT_qMRI - generic = file.with_name('generic.json') - with generic.open() as f: - all_data = json.load(f) - - algorithms = algorithm_information["algorithms"] - bvals = all_data.pop('config') - bvals = bvals['bvalues'] - for algorithm in algorithms: - first = True - for name, data in all_data.items(): - algorithm_dict = algorithm_information.get(algorithm, {}) - xfail = {"xfail": name in algorithm_dict.get("xfail_names", {}), - "strict": algorithm_dict.get("xfail_names", {}).get(name, True)} - kwargs = algorithm_dict.get("options", {}) - tolerances = algorithm_dict.get("tolerances", {}) - skiptime=False - if first == True: - if algorithm_dict.get("fail_first_time", {}) == True: - skiptime = True - first = False - if algorithm_dict.get("requieres_matlab", {}) == True: - if eng is None: - continue - else: - kwargs={**kwargs,'eng': eng} - yield name, bvals, data, algorithm, xfail, kwargs, tolerances, skiptime - -@pytest.mark.parametrize("name, bvals, data, algorithm, xfail, kwargs, tolerances, skiptime", data_ivim_fit_saved()) -def test_ivim_fit_saved(name, bvals, data, algorithm, xfail, kwargs, tolerances,skiptime, request, record_property): +def test_ivim_fit_saved(data_ivim_fit_saved, eng, request, record_property): + name, bvals, data, algorithm, xfail, kwargs, tolerances, skiptime, requires_matlab = data_ivim_fit_saved + max_time = 0.5 + if requires_matlab: + max_time = 2 + if eng is None: + print('test is here') + pytest.skip(reason="Running without matlab; if Matlab is available please run pytest --withmatlab") + else: + print('test is not here') + kwargs = {**kwargs, 'eng': eng} if xfail["xfail"]: mark = pytest.mark.xfail(reason="xfail", strict=xfail["strict"]) request.node.add_marker(mark) signal = signal_helper(data["data"]) tolerances = tolerances_helper(tolerances, data) - start_time = time.time() # Record the start time fit = OsipiBase(algorithm=algorithm, **kwargs) + start_time = time.time() # Record the start time fit_result = fit.osipi_fit(signal, bvals) elapsed_time = time.time() - start_time # Calculate elapsed time def to_list_if_needed(value): @@ -99,29 +72,18 @@ def to_list_if_needed(value): npt.assert_allclose(fit_result['Dp'],data['Dp'], rtol=tolerances["rtol"]["Dp"], atol=tolerances["atol"]["Dp"]) #assert fit_result['D'] < fit_result['Dp'], f"D {fit_result['D']} is larger than D* {fit_result['Dp']} for {name}" if not skiptime: - assert elapsed_time < 0.5, f"Algorithm {name} took {elapsed_time} seconds, which is longer than 2 second to fit per voxel" #less than 0.5 seconds per voxel - - -def algorithmlist(): - # Find the algorithms from algorithms.json - file = pathlib.Path(__file__) - algorithm_path = file.with_name('algorithms.json') - with algorithm_path.open() as f: - algorithm_information = json.load(f) - algorithms = algorithm_information["algorithms"] - for algorithm in algorithms: - algorithm_dict = algorithm_information.get(algorithm, {}) - args={} - if algorithm_dict.get("requieres_matlab", {}) == True: - if eng is None: - continue - else: - args = {**args, 'eng': eng} - yield algorithm, args - -@pytest.mark.parametrize("algorithm, args", algorithmlist()) -def test_default_bounds_and_initial_guesses(algorithm, args): - fit = OsipiBase(algorithm=algorithm,**args) + assert elapsed_time < max_time, f"Algorithm {name} took {elapsed_time} seconds, which is longer than 2 second to fit per voxel" #less than 0.5 seconds per voxel + +def test_default_bounds_and_initial_guesses(algorithmlist,eng): + algorithm, requires_matlab = algorithmlist + if requires_matlab: + if eng is None: + pytest.skip(reason="Running without matlab; if Matlab is available please run pytest --withmatlab") + else: + kwargs = {'eng': eng} + else: + kwargs={} + fit = OsipiBase(algorithm=algorithm,**kwargs) #assert fit.bounds is not None, f"For {algorithm}, there is no default fit boundary" #assert fit.initial_guess is not None, f"For {algorithm}, there is no default fit initial guess" if fit.use_bounds: @@ -141,38 +103,13 @@ def test_default_bounds_and_initial_guesses(algorithm, args): assert 0.9 <= fit.initial_guess[3] <= 1.1, f"For {algorithm}, the default initial guess for S {fit.initial_guess[3]} is unrealistic; note signal is normalized" -def bound_input(): - # Find the algorithms from algorithms.json - file = pathlib.Path(__file__) - algorithm_path = file.with_name('algorithms.json') - with algorithm_path.open() as f: - algorithm_information = json.load(f) - - # Load generic test data generated from the included phantom: phantoms/MR_XCAT_qMRI - generic = file.with_name('generic.json') - with generic.open() as f: - all_data = json.load(f) - - algorithms = algorithm_information["algorithms"] - bvals = all_data.pop('config') - bvals = bvals['bvalues'] - for name, data in all_data.items(): - for algorithm in algorithms: - algorithm_dict = algorithm_information.get(algorithm, {}) - xfail = {"xfail": name in algorithm_dict.get("xfail_names", {}), - "strict": algorithm_dict.get("xfail_names", {}).get(name, True)} - kwargs = algorithm_dict.get("options", {}) - tolerances = algorithm_dict.get("tolerances", {}) - if algorithm_dict.get("requieres_matlab", {}) == True: - if eng is None: - continue - else: - kwargs = {**kwargs, 'eng': eng} - yield name, bvals, data, algorithm, xfail, kwargs, tolerances - - -@pytest.mark.parametrize("name, bvals, data, algorithm, xfail, kwargs, tolerances", bound_input()) -def test_bounds(name, bvals, data, algorithm, xfail, kwargs, tolerances, request): +def test_bounds(bound_input, eng): + name, bvals, data, algorithm, xfail, kwargs, tolerances, requires_matlab = bound_input + if requires_matlab: + if eng is None: + pytest.skip(reason="Running without matlab; if Matlab is available please run pytest --withmatlab") + else: + kwargs = {**kwargs, 'eng': eng} bounds = ([0.0008, 0.2, 0.01, 1.1], [0.0012, 0.3, 0.02, 1.3]) # deliberately have silly bounds to see whether they are used fit = OsipiBase(algorithm=algorithm, bounds=bounds, initial_guess = [0.001, 0.25, 0.015, 1.2], **kwargs) diff --git a/tests/IVIMmodels/unit_tests/test_ivim_synthetic.py b/tests/IVIMmodels/unit_tests/test_ivim_synthetic.py index 20741327..0534f82e 100644 --- a/tests/IVIMmodels/unit_tests/test_ivim_synthetic.py +++ b/tests/IVIMmodels/unit_tests/test_ivim_synthetic.py @@ -14,8 +14,12 @@ #run using pytest --saveFileName test_output.txt --SNR 50 100 200 #e.g. pytest -m slow tests/IVIMmodels/unit_tests/test_ivim_synthetic.py --saveFileName test_output.csv --SNR 10 50 100 200 --fitCount 20 @pytest.mark.slow -def test_generated(ivim_algorithm, ivim_data, SNR, rtol, atol, fit_count, rician_noise, save_file, save_duration_file, use_prior): +def test_generated(algorithmlist, skip_list, ivim_data, SNR, rtol, atol, fit_count, rician_noise, save_file, save_duration_file, use_prior): # assert save_file == "test" + ivim_algorithm, requires_matlab = algorithmlist + if requires_matlab: + if eng is None: + pytest.skip(reason="Running without matlab; if Matlab is available please run pytest --withmatlab") rng = np.random.RandomState(42) # random.seed(42) S0 = 1 From 1d48e6cd7b976203eac57a6f3bec0d589597746a Mon Sep 17 00:00:00 2001 From: Oliver Gurney-Champion Date: Wed, 11 Jun 2025 14:55:20 +0200 Subject: [PATCH 18/18] fixed stuff --- conftest.py | 19 +++++-------------- tests/IVIMmodels/unit_tests/test_ivim_fit.py | 2 -- .../unit_tests/test_ivim_synthetic.py | 5 ++--- 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/conftest.py b/conftest.py index 598820e2..3c33bda6 100644 --- a/conftest.py +++ b/conftest.py @@ -215,14 +215,11 @@ def data_ivim_fit_saved(datafile, algorithmFile): kwargs = algorithm_dict.get("options", {}) tolerances = algorithm_dict.get("tolerances", {}) skiptime=False - if first == True: - if algorithm_dict.get("fail_first_time", {}) == True: + if first: + if algorithm_dict.get("fail_first_time", False): skiptime = True first = False - if algorithm_dict.get("requires_matlab", False) == True: - requires_matlab = True - else: - requires_matlab = False + requires_matlab = algorithm_dict.get("requires_matlab", False) yield name, bvals, data, algorithm, xfail, kwargs, tolerances, skiptime, requires_matlab def algorithmlist(algorithmFile): @@ -235,10 +232,7 @@ def algorithmlist(algorithmFile): algorithms = algorithm_information["algorithms"] for algorithm in algorithms: algorithm_dict = algorithm_information.get(algorithm, {}) - if algorithm_dict.get("requires_matlab", False) == True: - requires_matlab = True - else: - requires_matlab = False + requires_matlab = algorithm_dict.get("requires_matlab", False) yield algorithm, requires_matlab def bound_input(datafile,algorithmFile): @@ -261,8 +255,5 @@ def bound_input(datafile,algorithmFile): "strict": algorithm_dict.get("xfail_names", {}).get(name, True)} kwargs = algorithm_dict.get("options", {}) tolerances = algorithm_dict.get("tolerances", {}) - if algorithm_dict.get("requires_matlab", False) == True: - requires_matlab = True - else: - requires_matlab = False + requires_matlab = algorithm_dict.get("requires_matlab", False) yield name, bvals, data, algorithm, xfail, kwargs, tolerances, requires_matlab \ No newline at end of file diff --git a/tests/IVIMmodels/unit_tests/test_ivim_fit.py b/tests/IVIMmodels/unit_tests/test_ivim_fit.py index 350d85af..ea9b336b 100644 --- a/tests/IVIMmodels/unit_tests/test_ivim_fit.py +++ b/tests/IVIMmodels/unit_tests/test_ivim_fit.py @@ -36,10 +36,8 @@ def test_ivim_fit_saved(data_ivim_fit_saved, eng, request, record_property): if requires_matlab: max_time = 2 if eng is None: - print('test is here') pytest.skip(reason="Running without matlab; if Matlab is available please run pytest --withmatlab") else: - print('test is not here') kwargs = {**kwargs, 'eng': eng} if xfail["xfail"]: mark = pytest.mark.xfail(reason="xfail", strict=xfail["strict"]) diff --git a/tests/IVIMmodels/unit_tests/test_ivim_synthetic.py b/tests/IVIMmodels/unit_tests/test_ivim_synthetic.py index 0534f82e..6f2cb7c1 100644 --- a/tests/IVIMmodels/unit_tests/test_ivim_synthetic.py +++ b/tests/IVIMmodels/unit_tests/test_ivim_synthetic.py @@ -17,9 +17,8 @@ def test_generated(algorithmlist, skip_list, ivim_data, SNR, rtol, atol, fit_count, rician_noise, save_file, save_duration_file, use_prior): # assert save_file == "test" ivim_algorithm, requires_matlab = algorithmlist - if requires_matlab: - if eng is None: - pytest.skip(reason="Running without matlab; if Matlab is available please run pytest --withmatlab") + if requires_matlab and eng is None: + pytest.skip(reason="Running without matlab; if Matlab is available please run pytest --withmatlab") rng = np.random.RandomState(42) # random.seed(42) S0 = 1