Skip to content

Commit c32bfbd

Browse files
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
1 parent b7f2f8e commit c32bfbd

File tree

5 files changed

+137
-135
lines changed

5 files changed

+137
-135
lines changed

conftest.py

Lines changed: 94 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import csv
55
# import datetime
66

7+
78
def pytest_addoption(parser):
89
parser.addoption(
910
"--SNR",
@@ -87,15 +88,17 @@ def pytest_addoption(parser):
8788
help="Run MATLAB-dependent tests"
8889
)
8990

90-
eng = None
9191

92-
def pytest_configure(config):
93-
global eng
94-
if config.getoption("--withmatlab"):
95-
import matlab.engine
96-
print("Starting MATLAB engine...")
97-
eng = matlab.engine.start_matlab()
98-
print("MATLAB engine started.")
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
99102

100103

101104
@pytest.fixture(scope="session")
@@ -164,37 +167,102 @@ def use_prior(request):
164167
def pytest_generate_tests(metafunc):
165168
if "SNR" in metafunc.fixturenames:
166169
metafunc.parametrize("SNR", metafunc.config.getoption("SNR"))
167-
if "ivim_algorithm" in metafunc.fixturenames:
168-
algorithms = algorithm_list(metafunc.config.getoption("algorithmFile"), metafunc.config.getoption("selectAlgorithm"), metafunc.config.getoption("dropAlgorithm"))
169-
metafunc.parametrize("ivim_algorithm", algorithms)
170170
if "ivim_data" in metafunc.fixturenames:
171171
data = data_list(metafunc.config.getoption("dataFile"))
172172
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)
182+
183+
184+
def data_list(filename):
185+
current_folder = pathlib.Path.cwd()
186+
data_path = current_folder / filename
187+
with data_path.open() as f:
188+
all_data = json.load(f)
189+
190+
bvals = all_data.pop('config')
191+
bvals = bvals['bvalues']
192+
for name, data in all_data.items():
193+
yield name, bvals, data
194+
173195

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 == True:
219+
if algorithm_dict.get("fail_first_time", {}) == True:
220+
skiptime = True
221+
first = False
222+
if algorithm_dict.get("requires_matlab", False) == True:
223+
requires_matlab = True
224+
else:
225+
requires_matlab = False
226+
yield name, bvals, data, algorithm, xfail, kwargs, tolerances, skiptime, requires_matlab
174227

175-
def algorithm_list(filename, selected, dropped):
228+
def algorithmlist(algorithmFile):
229+
# Find the algorithms from algorithms.json
176230
current_folder = pathlib.Path.cwd()
177-
algorithm_path = current_folder / filename
231+
algorithm_path = current_folder / algorithmFile
178232
with algorithm_path.open() as f:
179233
algorithm_information = json.load(f)
180-
algorithms = set(algorithm_information["algorithms"])
234+
235+
algorithms = algorithm_information["algorithms"]
181236
for algorithm in algorithms:
182237
algorithm_dict = algorithm_information.get(algorithm, {})
183-
if algorithm_dict.get("requieres_matlab", {}) == True:
184-
if eng is None:
185-
algorithms = algorithms - set(algorithm)
186-
algorithms = algorithms - set(dropped)
187-
if len(selected) > 0 and selected[0]:
188-
algorithms = algorithms & set(selected)
189-
return list(algorithms)
238+
if algorithm_dict.get("requires_matlab", False) == True:
239+
requires_matlab = True
240+
else:
241+
requires_matlab = False
242+
yield algorithm, requires_matlab
190243

191-
def data_list(filename):
244+
def bound_input(datafile,algorithmFile):
245+
# Find the algorithms from algorithms.json
192246
current_folder = pathlib.Path.cwd()
193-
data_path = current_folder / filename
194-
with data_path.open() as f:
247+
algorithm_path = current_folder / algorithmFile
248+
with algorithm_path.open() as f:
249+
algorithm_information = json.load(f)
250+
# Load generic test data generated from the included phantom: phantoms/MR_XCAT_qMRI
251+
generic = current_folder / datafile
252+
with generic.open() as f:
195253
all_data = json.load(f)
196-
254+
algorithms = algorithm_information["algorithms"]
197255
bvals = all_data.pop('config')
198256
bvals = bvals['bvalues']
199257
for name, data in all_data.items():
200-
yield name, bvals, data
258+
for algorithm in algorithms:
259+
algorithm_dict = algorithm_information.get(algorithm, {})
260+
xfail = {"xfail": name in algorithm_dict.get("xfail_names", {}),
261+
"strict": algorithm_dict.get("xfail_names", {}).get(name, True)}
262+
kwargs = algorithm_dict.get("options", {})
263+
tolerances = algorithm_dict.get("tolerances", {})
264+
if algorithm_dict.get("requires_matlab", False) == True:
265+
requires_matlab = True
266+
else:
267+
requires_matlab = False
268+
yield name, bvals, data, algorithm, xfail, kwargs, tolerances, requires_matlab

src/standardized/ASD_MemorialSloanKettering_QAMPER_IVIM.py

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import numpy as np
33
import matlab.engine
44

5+
56
class ASD_MemorialSloanKettering_QAMPER_IVIM(OsipiBase):
67
"""
78
Bi-exponential fitting algorithm by Oliver Gurney-Champion, Amsterdam UMC
@@ -27,13 +28,12 @@ class ASD_MemorialSloanKettering_QAMPER_IVIM(OsipiBase):
2728
required_initial_guess_optional = True
2829
accepted_dimensions = 1 # Not sure how to define this for the number of accepted dimensions. Perhaps like the thresholds, at least and at most?
2930

30-
3131
# Supported inputs in the standardized class
3232
supported_bounds = True
3333
supported_initial_guess = True
3434
supported_thresholds = False
3535

36-
def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=None,eng=None):
36+
def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=None, eng=None):
3737
"""
3838
Everything this algorithm requires should be implemented here.
3939
Number of segmentation thresholds, bounds, etc.
@@ -52,8 +52,6 @@ def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=Non
5252
self.eng = eng
5353
self.keep_alive=True
5454

55-
56-
5755
def algorithm(self,dwi_arr, bval_arr, LB0, UB0, x0in):
5856
dwi_arr = matlab.double(dwi_arr.tolist())
5957
bval_arr = matlab.double(bval_arr.tolist())
@@ -104,24 +102,19 @@ def ivim_fit(self, signals, bvalues, **kwargs):
104102

105103
return results
106104

107-
108-
def __del__(self):
105+
def clean(self):
109106
if not self.keep_alive:
110107
if hasattr(self, "eng") and self.eng:
111108
try:
112109
self.eng.quit()
113110
except Exception as e:
114111
print(f"Warning: Failed to quit MATLAB engine cleanly: {e}")
115112

113+
def __del__(self):
114+
self.clean()
116115

117116
def __enter__(self):
118117
return self
119118

120-
121119
def __exit__(self, exc_type, exc_val, exc_tb):
122-
if not self.keep_alive:
123-
if hasattr(self, "eng") and self.eng:
124-
try:
125-
self.eng.quit()
126-
except Exception as e:
127-
print(f"Warning: Failed to quit MATLAB engine cleanly: {e}")
120+
self.clean()

tests/IVIMmodels/unit_tests/algorithms.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"OJ_GU_seg"
1717
],
1818
"ASD_MemorialSloanKettering_QAMPER_IVIM": {
19-
"requieres_matlab": true
19+
"requires_matlab": true
2020
},
2121
"IAR_LU_biexp": {
2222
"fail_first_time": true

tests/IVIMmodels/unit_tests/test_ivim_fit.py

Lines changed: 31 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import pathlib
66
import time
77
from src.wrappers.OsipiBase import OsipiBase
8-
from conftest import eng
98
#run using python -m pytest from the root folder
109

1110

@@ -31,50 +30,24 @@ def tolerances_helper(tolerances, data):
3130
tolerances["atol"] = tolerances.get("atol", {"f": 2e-1, "D": 5e-4, "Dp": 4e-2})
3231
return tolerances
3332

34-
def data_ivim_fit_saved():
35-
# Find the algorithms from algorithms.json
36-
file = pathlib.Path(__file__)
37-
algorithm_path = file.with_name('algorithms.json')
38-
with algorithm_path.open() as f:
39-
algorithm_information = json.load(f)
40-
41-
# Load generic test data generated from the included phantom: phantoms/MR_XCAT_qMRI
42-
generic = file.with_name('generic.json')
43-
with generic.open() as f:
44-
all_data = json.load(f)
45-
46-
algorithms = algorithm_information["algorithms"]
47-
bvals = all_data.pop('config')
48-
bvals = bvals['bvalues']
49-
for algorithm in algorithms:
50-
first = True
51-
for name, data in all_data.items():
52-
algorithm_dict = algorithm_information.get(algorithm, {})
53-
xfail = {"xfail": name in algorithm_dict.get("xfail_names", {}),
54-
"strict": algorithm_dict.get("xfail_names", {}).get(name, True)}
55-
kwargs = algorithm_dict.get("options", {})
56-
tolerances = algorithm_dict.get("tolerances", {})
57-
skiptime=False
58-
if first == True:
59-
if algorithm_dict.get("fail_first_time", {}) == True:
60-
skiptime = True
61-
first = False
62-
if algorithm_dict.get("requieres_matlab", {}) == True:
63-
if eng is None:
64-
continue
65-
else:
66-
kwargs={**kwargs,'eng': eng}
67-
yield name, bvals, data, algorithm, xfail, kwargs, tolerances, skiptime
68-
69-
@pytest.mark.parametrize("name, bvals, data, algorithm, xfail, kwargs, tolerances, skiptime", data_ivim_fit_saved())
70-
def test_ivim_fit_saved(name, bvals, data, algorithm, xfail, kwargs, tolerances,skiptime, request, record_property):
33+
def test_ivim_fit_saved(data_ivim_fit_saved, eng, request, record_property):
34+
name, bvals, data, algorithm, xfail, kwargs, tolerances, skiptime, requires_matlab = data_ivim_fit_saved
35+
max_time = 0.5
36+
if requires_matlab:
37+
max_time = 2
38+
if eng is None:
39+
print('test is here')
40+
pytest.skip(reason="Running without matlab; if Matlab is available please run pytest --withmatlab")
41+
else:
42+
print('test is not here')
43+
kwargs = {**kwargs, 'eng': eng}
7144
if xfail["xfail"]:
7245
mark = pytest.mark.xfail(reason="xfail", strict=xfail["strict"])
7346
request.node.add_marker(mark)
7447
signal = signal_helper(data["data"])
7548
tolerances = tolerances_helper(tolerances, data)
76-
start_time = time.time() # Record the start time
7749
fit = OsipiBase(algorithm=algorithm, **kwargs)
50+
start_time = time.time() # Record the start time
7851
fit_result = fit.osipi_fit(signal, bvals)
7952
elapsed_time = time.time() - start_time # Calculate elapsed time
8053
def to_list_if_needed(value):
@@ -99,29 +72,18 @@ def to_list_if_needed(value):
9972
npt.assert_allclose(fit_result['Dp'],data['Dp'], rtol=tolerances["rtol"]["Dp"], atol=tolerances["atol"]["Dp"])
10073
#assert fit_result['D'] < fit_result['Dp'], f"D {fit_result['D']} is larger than D* {fit_result['Dp']} for {name}"
10174
if not skiptime:
102-
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
103-
104-
105-
def algorithmlist():
106-
# Find the algorithms from algorithms.json
107-
file = pathlib.Path(__file__)
108-
algorithm_path = file.with_name('algorithms.json')
109-
with algorithm_path.open() as f:
110-
algorithm_information = json.load(f)
111-
algorithms = algorithm_information["algorithms"]
112-
for algorithm in algorithms:
113-
algorithm_dict = algorithm_information.get(algorithm, {})
114-
args={}
115-
if algorithm_dict.get("requieres_matlab", {}) == True:
116-
if eng is None:
117-
continue
118-
else:
119-
args = {**args, 'eng': eng}
120-
yield algorithm, args
121-
122-
@pytest.mark.parametrize("algorithm, args", algorithmlist())
123-
def test_default_bounds_and_initial_guesses(algorithm, args):
124-
fit = OsipiBase(algorithm=algorithm,**args)
75+
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
76+
77+
def test_default_bounds_and_initial_guesses(algorithmlist,eng):
78+
algorithm, requires_matlab = algorithmlist
79+
if requires_matlab:
80+
if eng is None:
81+
pytest.skip(reason="Running without matlab; if Matlab is available please run pytest --withmatlab")
82+
else:
83+
kwargs = {'eng': eng}
84+
else:
85+
kwargs={}
86+
fit = OsipiBase(algorithm=algorithm,**kwargs)
12587
#assert fit.bounds is not None, f"For {algorithm}, there is no default fit boundary"
12688
#assert fit.initial_guess is not None, f"For {algorithm}, there is no default fit initial guess"
12789
if fit.use_bounds:
@@ -141,38 +103,13 @@ def test_default_bounds_and_initial_guesses(algorithm, args):
141103
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"
142104

143105

144-
def bound_input():
145-
# Find the algorithms from algorithms.json
146-
file = pathlib.Path(__file__)
147-
algorithm_path = file.with_name('algorithms.json')
148-
with algorithm_path.open() as f:
149-
algorithm_information = json.load(f)
150-
151-
# Load generic test data generated from the included phantom: phantoms/MR_XCAT_qMRI
152-
generic = file.with_name('generic.json')
153-
with generic.open() as f:
154-
all_data = json.load(f)
155-
156-
algorithms = algorithm_information["algorithms"]
157-
bvals = all_data.pop('config')
158-
bvals = bvals['bvalues']
159-
for name, data in all_data.items():
160-
for algorithm in algorithms:
161-
algorithm_dict = algorithm_information.get(algorithm, {})
162-
xfail = {"xfail": name in algorithm_dict.get("xfail_names", {}),
163-
"strict": algorithm_dict.get("xfail_names", {}).get(name, True)}
164-
kwargs = algorithm_dict.get("options", {})
165-
tolerances = algorithm_dict.get("tolerances", {})
166-
if algorithm_dict.get("requieres_matlab", {}) == True:
167-
if eng is None:
168-
continue
169-
else:
170-
kwargs = {**kwargs, 'eng': eng}
171-
yield name, bvals, data, algorithm, xfail, kwargs, tolerances
172-
173-
174-
@pytest.mark.parametrize("name, bvals, data, algorithm, xfail, kwargs, tolerances", bound_input())
175-
def test_bounds(name, bvals, data, algorithm, xfail, kwargs, tolerances, request):
106+
def test_bounds(bound_input, eng):
107+
name, bvals, data, algorithm, xfail, kwargs, tolerances, requires_matlab = bound_input
108+
if requires_matlab:
109+
if eng is None:
110+
pytest.skip(reason="Running without matlab; if Matlab is available please run pytest --withmatlab")
111+
else:
112+
kwargs = {**kwargs, 'eng': eng}
176113
bounds = ([0.0008, 0.2, 0.01, 1.1], [0.0012, 0.3, 0.02, 1.3])
177114
# deliberately have silly bounds to see whether they are used
178115
fit = OsipiBase(algorithm=algorithm, bounds=bounds, initial_guess = [0.001, 0.25, 0.015, 1.2], **kwargs)

tests/IVIMmodels/unit_tests/test_ivim_synthetic.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,12 @@
1414
#run using pytest <path_to_this_file> --saveFileName test_output.txt --SNR 50 100 200
1515
#e.g. pytest -m slow tests/IVIMmodels/unit_tests/test_ivim_synthetic.py --saveFileName test_output.csv --SNR 10 50 100 200 --fitCount 20
1616
@pytest.mark.slow
17-
def test_generated(ivim_algorithm, ivim_data, SNR, rtol, atol, fit_count, rician_noise, save_file, save_duration_file, use_prior):
17+
def test_generated(algorithmlist, skip_list, ivim_data, SNR, rtol, atol, fit_count, rician_noise, save_file, save_duration_file, use_prior):
1818
# assert save_file == "test"
19+
ivim_algorithm, requires_matlab = algorithmlist
20+
if requires_matlab:
21+
if eng is None:
22+
pytest.skip(reason="Running without matlab; if Matlab is available please run pytest --withmatlab")
1923
rng = np.random.RandomState(42)
2024
# random.seed(42)
2125
S0 = 1

0 commit comments

Comments
 (0)