Skip to content

Commit da7e8c9

Browse files
authored
Merge pull request #103 from OSIPI/unit_testing_Matlab
Matlab installation + adding Amita's code to wrapper
2 parents 0785232 + 1d48e6c commit da7e8c9

File tree

8 files changed

+258
-95
lines changed

8 files changed

+258
-95
lines changed

.gitignore

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ md5sums.txt
1919
*.asv
2020
src/original/ASD_MemorialSloanKettering/MRI-QAMPER_IVIM/test_data
2121
src/original/ASD_MemorialSloanKettering/MRI-QAMPER_IVIM/output_files
22-
23-
22+
tests/IVIMmodels/unit_tests/*.log
23+
junit/*
24+
ivim_simulation.bval
25+
ivim_simulation.bvec
2426

2527
# Unit test / coverage reports
2628
.tox/

conftest.py

Lines changed: 93 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,24 @@ def pytest_addoption(parser):
8181
type=str,
8282
help="Drop this algorithm from the list"
8383
)
84+
parser.addoption(
85+
"--withmatlab",
86+
action="store_true",
87+
default=False,
88+
help="Run MATLAB-dependent tests"
89+
)
90+
91+
92+
@pytest.fixture(scope="session")
93+
def eng(request):
94+
"""Start and return a MATLAB engine session if --withmatlab is set."""
95+
if not request.config.getoption("--withmatlab"):
96+
return None
97+
import matlab.engine
98+
print("Starting MATLAB engine...")
99+
eng = matlab.engine.start_matlab()
100+
print("MATLAB engine started.")
101+
return eng
84102

85103

86104
@pytest.fixture(scope="session")
@@ -149,25 +167,20 @@ def use_prior(request):
149167
def pytest_generate_tests(metafunc):
150168
if "SNR" in metafunc.fixturenames:
151169
metafunc.parametrize("SNR", metafunc.config.getoption("SNR"))
152-
if "ivim_algorithm" in metafunc.fixturenames:
153-
algorithms = algorithm_list(metafunc.config.getoption("algorithmFile"), metafunc.config.getoption("selectAlgorithm"), metafunc.config.getoption("dropAlgorithm"))
154-
metafunc.parametrize("ivim_algorithm", algorithms)
155170
if "ivim_data" in metafunc.fixturenames:
156171
data = data_list(metafunc.config.getoption("dataFile"))
157172
metafunc.parametrize("ivim_data", data)
173+
if "data_ivim_fit_saved" in metafunc.fixturenames:
174+
args = data_ivim_fit_saved(metafunc.config.getoption("dataFile"),metafunc.config.getoption("algorithmFile"))
175+
metafunc.parametrize("data_ivim_fit_saved", args)
176+
if "algorithmlist" in metafunc.fixturenames:
177+
args = algorithmlist(metafunc.config.getoption("algorithmFile"))
178+
metafunc.parametrize("algorithmlist", args)
179+
if "bound_input" in metafunc.fixturenames:
180+
args = bound_input(metafunc.config.getoption("dataFile"),metafunc.config.getoption("algorithmFile"))
181+
metafunc.parametrize("bound_input", args)
158182

159183

160-
def algorithm_list(filename, selected, dropped):
161-
current_folder = pathlib.Path.cwd()
162-
algorithm_path = current_folder / filename
163-
with algorithm_path.open() as f:
164-
algorithm_information = json.load(f)
165-
algorithms = set(algorithm_information["algorithms"])
166-
algorithms = algorithms - set(dropped)
167-
if len(selected) > 0 and selected[0]:
168-
algorithms = algorithms & set(selected)
169-
return list(algorithms)
170-
171184
def data_list(filename):
172185
current_folder = pathlib.Path.cwd()
173186
data_path = current_folder / filename
@@ -178,3 +191,69 @@ def data_list(filename):
178191
bvals = bvals['bvalues']
179192
for name, data in all_data.items():
180193
yield name, bvals, data
194+
195+
196+
def data_ivim_fit_saved(datafile, algorithmFile):
197+
# Find the algorithms from algorithms.json
198+
current_folder = pathlib.Path.cwd()
199+
algorithm_path = current_folder / algorithmFile
200+
with algorithm_path.open() as f:
201+
algorithm_information = json.load(f)
202+
# Load generic test data generated from the included phantom: phantoms/MR_XCAT_qMRI
203+
generic = current_folder / datafile
204+
with generic.open() as f:
205+
all_data = json.load(f)
206+
algorithms = algorithm_information["algorithms"]
207+
bvals = all_data.pop('config')
208+
bvals = bvals['bvalues']
209+
for algorithm in algorithms:
210+
first = True
211+
for name, data in all_data.items():
212+
algorithm_dict = algorithm_information.get(algorithm, {})
213+
xfail = {"xfail": name in algorithm_dict.get("xfail_names", {}),
214+
"strict": algorithm_dict.get("xfail_names", {}).get(name, True)}
215+
kwargs = algorithm_dict.get("options", {})
216+
tolerances = algorithm_dict.get("tolerances", {})
217+
skiptime=False
218+
if first:
219+
if algorithm_dict.get("fail_first_time", False):
220+
skiptime = True
221+
first = False
222+
requires_matlab = algorithm_dict.get("requires_matlab", False)
223+
yield name, bvals, data, algorithm, xfail, kwargs, tolerances, skiptime, requires_matlab
224+
225+
def algorithmlist(algorithmFile):
226+
# Find the algorithms from algorithms.json
227+
current_folder = pathlib.Path.cwd()
228+
algorithm_path = current_folder / algorithmFile
229+
with algorithm_path.open() as f:
230+
algorithm_information = json.load(f)
231+
232+
algorithms = algorithm_information["algorithms"]
233+
for algorithm in algorithms:
234+
algorithm_dict = algorithm_information.get(algorithm, {})
235+
requires_matlab = algorithm_dict.get("requires_matlab", False)
236+
yield algorithm, requires_matlab
237+
238+
def bound_input(datafile,algorithmFile):
239+
# Find the algorithms from algorithms.json
240+
current_folder = pathlib.Path.cwd()
241+
algorithm_path = current_folder / algorithmFile
242+
with algorithm_path.open() as f:
243+
algorithm_information = json.load(f)
244+
# Load generic test data generated from the included phantom: phantoms/MR_XCAT_qMRI
245+
generic = current_folder / datafile
246+
with generic.open() as f:
247+
all_data = json.load(f)
248+
algorithms = algorithm_information["algorithms"]
249+
bvals = all_data.pop('config')
250+
bvals = bvals['bvalues']
251+
for name, data in all_data.items():
252+
for algorithm in algorithms:
253+
algorithm_dict = algorithm_information.get(algorithm, {})
254+
xfail = {"xfail": name in algorithm_dict.get("xfail_names", {}),
255+
"strict": algorithm_dict.get("xfail_names", {}).get(name, True)}
256+
kwargs = algorithm_dict.get("options", {})
257+
tolerances = algorithm_dict.get("tolerances", {})
258+
requires_matlab = algorithm_dict.get("requires_matlab", False)
259+
yield name, bvals, data, algorithm, xfail, kwargs, tolerances, requires_matlab

pytest.ini

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
markers =
33
slow: marks tests as slow (deselect with '-m "not slow"')
44
addopts =
5-
-m 'not slow'
5+
-m 'not slow'
6+
testpaths = tests
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
from src.wrappers.OsipiBase import OsipiBase
2+
import numpy as np
3+
import matlab.engine
4+
5+
6+
class ASD_MemorialSloanKettering_QAMPER_IVIM(OsipiBase):
7+
"""
8+
Bi-exponential fitting algorithm by Oliver Gurney-Champion, Amsterdam UMC
9+
"""
10+
11+
# I'm thinking that we define default attributes for each submission like this
12+
# And in __init__, we can call the OsipiBase control functions to check whether
13+
# the user inputs fulfil the requirements
14+
15+
# Some basic stuff that identifies the algorithm
16+
id_author = "LoCastro, Dr. Ramesh Paudyal, Dr. Amita Shukla-Dave"
17+
id_algorithm_type = "Bi-exponential fit"
18+
id_return_parameters = "f, D*, D, S0"
19+
id_units = "seconds per milli metre squared or milliseconds per micro metre squared"
20+
21+
# Algorithm requirements
22+
required_bvalues = 4
23+
required_thresholds = [0,
24+
0] # Interval from "at least" to "at most", in case submissions allow a custom number of thresholds
25+
required_bounds = False
26+
required_bounds_optional = True # Bounds may not be required but are optional
27+
required_initial_guess = False
28+
required_initial_guess_optional = True
29+
accepted_dimensions = 1 # Not sure how to define this for the number of accepted dimensions. Perhaps like the thresholds, at least and at most?
30+
31+
# Supported inputs in the standardized class
32+
supported_bounds = True
33+
supported_initial_guess = True
34+
supported_thresholds = False
35+
36+
def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=None, eng=None):
37+
"""
38+
Everything this algorithm requires should be implemented here.
39+
Number of segmentation thresholds, bounds, etc.
40+
41+
Our OsipiBase object could contain functions that compare the inputs with
42+
the requirements.
43+
"""
44+
#super(OGC_AmsterdamUMC_biexp, self).__init__(bvalues, bounds, initial_guess, fitS0)
45+
super(ASD_MemorialSloanKettering_QAMPER_IVIM, self).__init__(bvalues=bvalues, bounds=bounds, initial_guess=initial_guess)
46+
self.initialize(bounds, initial_guess)
47+
if eng is None:
48+
print('initiating matlab; this may take some time. For repeated testing one could use the optional input eng as an already initiated matlab engine')
49+
self.eng=matlab.engine.start_matlab()
50+
self.keep_alive=False
51+
else:
52+
self.eng = eng
53+
self.keep_alive=True
54+
55+
def algorithm(self,dwi_arr, bval_arr, LB0, UB0, x0in):
56+
dwi_arr = matlab.double(dwi_arr.tolist())
57+
bval_arr = matlab.double(bval_arr.tolist())
58+
LB0 = matlab.double(LB0.tolist())
59+
UB0 = matlab.double(UB0.tolist())
60+
x0in = matlab.double(x0in.tolist())
61+
results = self.eng.IVIM_standard_bcin(
62+
dwi_arr, bval_arr, 0.0, LB0, UB0, x0in, False, 0, 0,nargout=11)
63+
(f_arr, D_arr, Dx_arr, s0_arr, fitted_dwi_arr, RSS, rms_val, chi, AIC, BIC, R_sq) = results
64+
return D_arr/1000, f_arr, Dx_arr/1000, s0_arr
65+
66+
def initialize(self, bounds, initial_guess):
67+
if bounds is None:
68+
print('warning, no bounds were defined, so algorithm-specific default bounds are used')
69+
self.bounds=([1e-6, 0, 0.004, 0],[0.003, 1.0, 0.2, 5])
70+
else:
71+
self.bounds=bounds
72+
if initial_guess is None:
73+
print('warning, no initial guesses were defined, so algorithm-specific default initial guess is used')
74+
self.initial_guess = [0.001, 0.2, 0.01, 1]
75+
else:
76+
self.initial_guess = initial_guess
77+
self.use_initial_guess = True
78+
self.use_initial_guess = True
79+
self.use_bounds = True
80+
81+
def ivim_fit(self, signals, bvalues, **kwargs):
82+
"""Perform the IVIM fit
83+
84+
Args:
85+
signals (array-like)
86+
bvalues (array-like, optional): b-values for the signals. If None, self.bvalues will be used. Default is None.
87+
88+
Returns:
89+
_type_: _description_
90+
"""
91+
92+
bvalues=np.array(bvalues)
93+
LB = np.array(self.bounds[0])[[1,0,2,3]]
94+
UB = np.array(self.bounds[1])[[1,0,2,3]]
95+
96+
fit_results = self.algorithm(np.array(signals)[:,np.newaxis], bvalues, LB, UB, np.array(self.initial_guess)[[1,0,2,3]])
97+
98+
results = {}
99+
results["D"] = fit_results[0]
100+
results["f"] = fit_results[1]
101+
results["Dp"] = fit_results[2]
102+
103+
return results
104+
105+
def clean(self):
106+
if not self.keep_alive:
107+
if hasattr(self, "eng") and self.eng:
108+
try:
109+
self.eng.quit()
110+
except Exception as e:
111+
print(f"Warning: Failed to quit MATLAB engine cleanly: {e}")
112+
113+
def __del__(self):
114+
self.clean()
115+
116+
def __enter__(self):
117+
return self
118+
119+
def __exit__(self, exc_type, exc_val, exc_tb):
120+
self.clean()

src/standardized/OGC_AmsterdamUMC_biexp.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def initialize(self, bounds, initial_guess, fitS0):
5555
self.bounds=bounds
5656
if initial_guess is None:
5757
print('warning, no initial guesses were defined, so default bounds are used of [0.001, 0.001, 0.01, 1]')
58-
self.initial_guess = [0.001, 0.001, 0.01, 1]
58+
self.initial_guess = [0.001, 0.1, 0.01, 1]
5959
else:
6060
self.initial_guess = initial_guess
6161
self.use_initial_guess = True

tests/IVIMmodels/unit_tests/algorithms.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"algorithms": [
3+
"ASD_MemorialSloanKettering_QAMPER_IVIM",
34
"ETP_SRI_LinearFitting",
45
"IAR_LU_biexp",
56
"IAR_LU_modified_mix",
@@ -14,6 +15,9 @@
1415
"PvH_KB_NKI_IVIMfit",
1516
"OJ_GU_seg"
1617
],
18+
"ASD_MemorialSloanKettering_QAMPER_IVIM": {
19+
"requires_matlab": true
20+
},
1721
"IAR_LU_biexp": {
1822
"fail_first_time": true
1923
},

0 commit comments

Comments
 (0)