Skip to content

Commit ab000a7

Browse files
committed
Added a synthetic test and control from the command line and saving of those test results
1 parent b408532 commit ab000a7

File tree

5 files changed

+219
-34
lines changed

5 files changed

+219
-34
lines changed

conftest.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import pytest
2+
import pathlib
3+
import json
4+
import csv
5+
import datetime
6+
7+
8+
def pytest_addoption(parser):
9+
parser.addoption(
10+
"--SNR",
11+
default=[100],
12+
nargs="+",
13+
type=int,
14+
help="Evaluation test SNR",
15+
)
16+
parser.addoption(
17+
"--algorithmFile",
18+
default="tests/IVIMmodels/unit_tests/algorithms.json",
19+
type=str,
20+
help="Algorithm file name",
21+
)
22+
parser.addoption(
23+
"--dataFile",
24+
default="tests/IVIMmodels/unit_tests/generic.json",
25+
type=str,
26+
help="Default data file name",
27+
)
28+
parser.addoption(
29+
"--saveFileName",
30+
default="",
31+
type=str,
32+
help="Saved results file name",
33+
)
34+
parser.addoption(
35+
"--rtol",
36+
default=1,
37+
type=float,
38+
help="Relative tolerance",
39+
)
40+
parser.addoption(
41+
"--atol",
42+
default=1,
43+
type=float,
44+
help="Absolute tolerance",
45+
)
46+
47+
48+
@pytest.fixture(scope="session")
49+
def save_file(request):
50+
filename = request.config.getoption("--saveFileName")
51+
if filename:
52+
current_folder = pathlib.Path.cwd()
53+
filename = current_folder / filename
54+
# print(filename)
55+
# filename.unlink(missing_ok=True)
56+
filename = filename.as_posix()
57+
with open(filename, "a") as csv_file:
58+
writer = csv.writer(csv_file, delimiter='#')
59+
writer.writerow(["", datetime.datetime.now()])
60+
return filename
61+
62+
@pytest.fixture(scope="session")
63+
def rtol(request):
64+
return request.config.getoption("--rtol")
65+
66+
67+
@pytest.fixture(scope="session")
68+
def atol(request):
69+
return request.config.getoption("--atol")
70+
71+
72+
def pytest_generate_tests(metafunc):
73+
if "SNR" in metafunc.fixturenames:
74+
metafunc.parametrize("SNR", metafunc.config.getoption("SNR"))
75+
if "ivim_algorithm" in metafunc.fixturenames:
76+
algorithms = algorithm_list(metafunc.config.getoption("algorithmFile"))
77+
metafunc.parametrize("ivim_algorithm", algorithms)
78+
if "algorithm_file" in metafunc.fixturenames:
79+
metafunc.parametrize("algorithm_file", metafunc.config.getoption("algorithmFile"))
80+
if "ivim_data" in metafunc.fixturenames:
81+
data = data_list(metafunc.config.getoption("dataFile"))
82+
metafunc.parametrize("ivim_data", data)
83+
if "data_file" in metafunc.fixturenames:
84+
metafunc.parametrize("data_file", metafunc.config.getoption("dataFile"))
85+
86+
87+
def algorithm_list(filename):
88+
current_folder = pathlib.Path.cwd()
89+
algorithm_path = current_folder / filename
90+
with algorithm_path.open() as f:
91+
algorithm_information = json.load(f)
92+
return algorithm_information["algorithms"]
93+
94+
def data_list(filename):
95+
current_folder = pathlib.Path.cwd()
96+
data_path = current_folder / filename
97+
with data_path.open() as f:
98+
all_data = json.load(f)
99+
100+
bvals = all_data.pop('config')
101+
bvals = bvals['bvalues']
102+
for name, data in all_data.items():
103+
yield name, bvals, data

tests/IVIMmodels/__init__.py

Whitespace-only changes.

tests/IVIMmodels/unit_tests/__init__.py

Whitespace-only changes.

tests/IVIMmodels/unit_tests/test_ivim_fit.py

Lines changed: 79 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,57 @@
1010

1111
#run using python -m pytest from the root folder
1212

13+
14+
# @pytest.fixture
15+
# def algorithm_fixture()
16+
# def test_fixtures()
17+
18+
# use a fixture to generate data
19+
# either read a config file for the test or perhaps hard code a few fixtures and usefixtures in the config?
20+
# use a fixture to save data
21+
22+
# def algorithm_list():
23+
# # Find the algorithms from algorithms.json
24+
# file = pathlib.Path(__file__)
25+
# algorithm_path = file.with_name('algorithms.json')
26+
# with algorithm_path.open() as f:
27+
# algorithm_information = json.load(f)
28+
# return algorithm_information["algorithms"]
29+
30+
# @pytest.fixture(params=algorithm_list())
31+
# def algorithm_fixture(request):
32+
# # assert request.param == "algorithms"
33+
# yield request.param
34+
35+
36+
37+
# @pytest.fixture(params=SNR)
38+
# def noise_fixture(request):
39+
# return request.config.getoption("--noise")
40+
41+
# @pytest.fixture
42+
# def noise_fixture(request):
43+
# yield request.param
44+
45+
# @pytest.mark.parametrize("S", [SNR])
46+
# @pytest.mark.parametrize("D, Dp, f, bvals", [[0.0015, 0.1, 0.11000000000000007,[0, 5, 10, 50, 100, 200, 300, 500, 1000]]])
47+
# def test_generated(ivim_algorithm, ivim_data, SNR):
48+
# S0 = 1
49+
# gd = GenerateData()
50+
# name, bvals, data = ivim_data
51+
# D = data["D"]
52+
# f = data["f"]
53+
# Dp = data["Dp"]
54+
# if "data" not in data:
55+
# signal = gd.ivim_signal(D, Dp, f, S0, bvals, SNR)
56+
# else:
57+
# signal = data["data"]
58+
# fit = OsipiBase(algorithm=ivim_algorithm)
59+
# [f_fit, Dp_fit, D_fit] = fit.ivim_fit(signal, bvals)
60+
# npt.assert_allclose([f, D, Dp], [f_fit, D_fit, Dp_fit])
61+
62+
63+
1364
# test_linear_data = [
1465
# pytest.param(0, np.linspace(0, 1000, 11), id='0'),
1566
# pytest.param(0.01, np.linspace(0, 1000, 11), id='0.1'),
@@ -57,6 +108,32 @@
57108
#if not np.allclose(f, 0):
58109
#npt.assert_allclose(Dp, Dp_fit, rtol=1e-2, atol=1e-3)
59110

111+
112+
# convert the algorithm list and signal list to fixtures that read from the files into params (scope="session")
113+
# from that helpers can again parse the files?
114+
115+
def signal_helper(signal):
116+
signal = np.asarray(signal)
117+
signal = np.abs(signal)
118+
signal /= signal[0]
119+
ratio = 1 / signal[0]
120+
return signal, ratio
121+
122+
def tolerances_helper(tolerances, ratio, noise):
123+
if "dynamic_rtol" in tolerances:
124+
dyn_rtol = tolerances["dynamic_rtol"]
125+
scale = dyn_rtol["offset"] + dyn_rtol["ratio"]*ratio + dyn_rtol["noise"]*noise + dyn_rtol["noiseCrossRatio"]*ratio*noise
126+
tolerances["rtol"] = {"f": scale*dyn_rtol["f"], "D": scale*dyn_rtol["D"], "Dp": scale*dyn_rtol["Dp"]}
127+
else:
128+
tolerances["rtol"] = tolerances.get("rtol", {"f": 5, "D": 5, "Dp": 25})
129+
if "dynamic_atol" in tolerances:
130+
dyn_atol = tolerances["dynamic_atol"]
131+
scale = dyn_atol["offset"] + dyn_atol["ratio"]*ratio + dyn_atol["noise"]*noise + dyn_atol["noiseCrossRatio"]*ratio*noise
132+
tolerances["atol"] = {"f": scale*dyn_atol["f"], "D": scale*dyn_atol["D"], "Dp": scale*dyn_atol["Dp"]}
133+
else:
134+
tolerances["atol"] = tolerances.get("atol", {"f": 1e-2, "D": 1e-2, "Dp": 1e-1})
135+
return tolerances
136+
60137
def data_ivim_fit_saved():
61138
# Find the algorithms from algorithms.json
62139
file = pathlib.Path(__file__)
@@ -88,40 +165,8 @@ def test_ivim_fit_saved(name, bvals, data, algorithm, xfail, kwargs, tolerances,
88165
mark = pytest.mark.xfail(reason="xfail", strict=xfail["strict"])
89166
request.node.add_marker(mark)
90167
fit = OsipiBase(algorithm=algorithm, **kwargs)
91-
signal = np.asarray(data['data'])
92-
signal = np.abs(signal)
93-
signal /= signal[0]
94-
ratio = 1 / signal[0]
95-
# has_negatives = np.any(signal<0)
96-
if "dynamic_rtol" in tolerances:
97-
dyn_rtol = tolerances["dynamic_rtol"]
98-
scale = dyn_rtol["offset"] + dyn_rtol["ratio"]*ratio + dyn_rtol["noise"]*data["noise"] + dyn_rtol["noiseCrossRatio"]*ratio*data["noise"]
99-
tolerances["rtol"] = {"f": scale*dyn_rtol["f"], "D": scale*dyn_rtol["D"], "Dp": scale*dyn_rtol["Dp"]}
100-
else:
101-
tolerances["rtol"] = tolerances.get("rtol", {"f": 5, "D": 5, "Dp": 25})
102-
if "dynamic_atol" in tolerances:
103-
dyn_atol = tolerances["dynamic_atol"]
104-
scale = dyn_atol["offset"] + dyn_atol["ratio"]*ratio + dyn_atol["noise"]*data["noise"] + dyn_atol["noiseCrossRatio"]*ratio*data["noise"]
105-
tolerances["atol"] = {"f": scale*dyn_atol["f"], "D": scale*dyn_atol["D"], "Dp": scale*dyn_atol["Dp"]}
106-
else:
107-
tolerances["atol"] = tolerances.get("atol", {"f": 1e-2, "D": 1e-2, "Dp": 1e-1})
108-
# if tolerances:
109-
# # signal = np.abs(signal)
110-
# # ratio = 1 / signal[0]
111-
# # signal /= signal[0]
112-
# # tolerance = 1e-2 + 25 * data['noise'] * ratio # totally empirical
113-
# # tolerance = 1
114-
# tolerances = {"rtol": {"f": 5, "D": 5, "Dp": 25},
115-
# "atol": {"f": 1e-2, "D": 1e-2, "Dp": 1e-1}}
116-
#if has_negatives: # this fitting doesn't do well with negatives
117-
# tolerance += 1
118-
#if data['f'] == 1.0:
119-
#linear_fit = fit.ivim_fit(np.log(signal), bvals, linear_fit_option=True)
120-
#npt.assert_allclose([data['f'], data['Dp']], linear_fit, atol=tolerance)
121-
#else:
122-
#[f_fit, Dp_fit, D_fit] = fit.ivim_fit(signal, bvals)
123-
#npt.assert_allclose([data['f'], data['D']], [f_fit, D_fit], atol=tolerance)
124-
#npt.assert_allclose(data['Dp'], Dp_fit, atol=1e-1) # go easy on the perfusion as it's a linear fake
168+
signal, ratio = signal_helper(data["data"])
169+
tolerances = tolerances_helper(tolerances, ratio, data["noise"])
125170
[f_fit, Dp_fit, D_fit] = fit.ivim_fit(signal, bvals)
126171
npt.assert_allclose(data['f'], f_fit, rtol=tolerances["rtol"]["f"], atol=tolerances["atol"]["f"])
127172
npt.assert_allclose(data['D'], D_fit, rtol=tolerances["rtol"]["D"], atol=tolerances["atol"]["D"])
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import numpy as np
2+
import numpy.testing as npt
3+
import pytest
4+
import json
5+
import pathlib
6+
import os
7+
import csv
8+
9+
from src.wrappers.OsipiBase import OsipiBase
10+
from utilities.data_simulation.GenerateData import GenerateData
11+
12+
#run using pytest <path_to_this_file> --saveFileName test_output.txt --SNR 50 100 200
13+
#e.g. pytest tests/IVIMmodels/unit_tests/test_ivim_synthetic.py --saveFileName test_output.txt --SNR 50 100 200
14+
def test_generated(ivim_algorithm, ivim_data, SNR, rtol, atol, save_file):
15+
# assert save_file == "test"
16+
S0 = 1
17+
gd = GenerateData()
18+
name, bvals, data = ivim_data
19+
D = data["D"]
20+
f = data["f"]
21+
Dp = data["Dp"]
22+
# if "data" not in data:
23+
signal = gd.ivim_signal(D, Dp, f, S0, bvals, SNR)
24+
# else:
25+
# signal = data["data"]
26+
fit = OsipiBase(algorithm=ivim_algorithm)
27+
[f_fit, Dp_fit, D_fit] = fit.ivim_fit(signal, bvals)
28+
if save_file:
29+
save_results(save_file, ivim_algorithm, name, SNR, [f, D, Dp], [f_fit, Dp_fit, D_fit])
30+
npt.assert_allclose([f, D, Dp], [f_fit, D_fit, Dp_fit], rtol, atol)
31+
32+
33+
def save_results(filename, algorithm, name, SNR, truth, fit):
34+
with open(filename, "a") as csv_file:
35+
writer = csv.writer(csv_file, delimiter=',')
36+
data = [algorithm, name, SNR, truth, fit]
37+
writer.writerow(data)

0 commit comments

Comments
 (0)