Skip to content

Commit 6c8a829

Browse files
authored
Merge pull request #73 from AhmedBasem20/test-results-file
Save test results to a json file & Display results Visuals on the website
2 parents afbbe6c + 6b2b82e commit 6c8a829

File tree

6 files changed

+148
-106
lines changed

6 files changed

+148
-106
lines changed

.github/workflows/website.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,20 @@ jobs:
4646
with:
4747
name: 'Data'
4848

49+
- name: Run the test that generates the plots report.
50+
run: |
51+
pytest tests/IVIMmodels/unit_tests/test_ivim_fit.py --json-report
52+
mv .report.json utilities/
53+
python utilities/report-summary.py .report.json report-summary.json
54+
4955
- name: 'Filter and compress results file.'
5056
run: python utilities/reduce_output_size.py test_output.csv test_output.csv.gz
5157

5258
- name: move data to the dashboard folder
5359
run: |
5460
mv test_output.csv.gz website/dashboard
61+
mv report-summary.json website/dashboard
62+
5563
5664
- name: Build documentation
5765
run: |

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ tqdm
1313
pandas
1414
sphinx
1515
sphinx_rtd_theme
16+
pytest-json-report

tests/IVIMmodels/unit_tests/test_ivim_fit.py

Lines changed: 21 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -3,115 +3,11 @@
33
import pytest
44
import json
55
import pathlib
6-
import os
76

87
from src.wrappers.OsipiBase import OsipiBase
98
from utilities.data_simulation.GenerateData import GenerateData
10-
119
#run using python -m pytest from the root folder
1210

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.osipi_fit(signal, bvals)
60-
# npt.assert_allclose([f, D, Dp], [f_fit, D_fit, Dp_fit])
61-
62-
63-
64-
# test_linear_data = [
65-
# pytest.param(0, np.linspace(0, 1000, 11), id='0'),
66-
# pytest.param(0.01, np.linspace(0, 1000, 11), id='0.1'),
67-
# pytest.param(0.02, np.linspace(0, 1000, 11), id='0.2'),
68-
# pytest.param(0.03, np.linspace(0, 1000, 11), id='0.3'),
69-
# pytest.param(0.04, np.linspace(0, 1000, 11), id='0.4'),
70-
# pytest.param(0.05, np.linspace(0, 1000, 11), id='0.5'),
71-
# pytest.param(0.08, np.linspace(0, 1000, 11), id='0.8'),
72-
# pytest.param(0.1, np.linspace(0, 1000, 11), id='1'),
73-
# ]
74-
75-
#@pytest.mark.parametrize("D, bvals", test_linear_data)
76-
#def test_linear_fit(D, bvals):
77-
#gd = GenerateData()
78-
#gd_signal = gd.exponential_signal(D, bvals)
79-
#print(gd_signal)
80-
#fit = LinearFit()
81-
#D_fit = fit.linear_fit(bvals, np.log(gd_signal))
82-
#npt.assert_allclose([1, D], D_fit)
83-
84-
# test_ivim_data = [
85-
# pytest.param(0, 0.01, 0.05, np.linspace(0, 1000, 11), id='0'),
86-
# pytest.param(0.1, 0.01, 0.05, np.linspace(0, 1000, 11), id='0.1'),
87-
# pytest.param(0.2, 0.01, 0.05, np.linspace(0, 1000, 11), id='0.2'),
88-
# pytest.param(0.1, 0.05, 0.1, np.linspace(0, 1000, 11), id='0.3'),
89-
# pytest.param(0.4, 0.001, 0.05, np.linspace(0, 1000, 11), id='0.4'),
90-
# pytest.param(0.5, 0.001, 0.05, np.linspace(0, 1000, 11), id='0.5'),
91-
# ]
92-
93-
#@pytest.mark.parametrize("f, D, Dp, bvals", test_ivim_data)
94-
#def test_ivim_fit(f, D, Dp, bvals):
95-
## We should make a wrapper that runs this for a range of different settings, such as b thresholds, bounds, etc.
96-
## An additional inputs to these functions could perhaps be a "settings" class with attributes that are the settings to the
97-
## algorithms. I.e. bvalues, thresholds, bounds, initial guesses.
98-
## That way, we can write something that defines a range of settings, and then just run them through here.
99-
100-
#gd = GenerateData()
101-
#gd_signal = gd.ivim_signal(D, Dp, f, 1, bvals)
102-
103-
##fit = LinearFit() # This is the old code by ETP
104-
#fit = ETP_SRI_LinearFitting() # This is the standardized format by IAR, which every algorithm will be implemented with
105-
106-
#[f_fit, Dp_fit, D_fit] = fit.ivim_fit(gd_signal, bvals) # Note that I have transposed Dp and D. We should decide on a standard order for these. I usually go with f, Dp, and D ordered after size.
107-
#npt.assert_allclose([f, D], [f_fit, D_fit], atol=1e-5)
108-
#if not np.allclose(f, 0):
109-
#npt.assert_allclose(Dp, Dp_fit, rtol=1e-2, atol=1e-3)
110-
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-
11511
def signal_helper(signal):
11612
signal = np.asarray(signal)
11713
signal = np.abs(signal)
@@ -160,17 +56,36 @@ def data_ivim_fit_saved():
16056

16157

16258
@pytest.mark.parametrize("name, bvals, data, algorithm, xfail, kwargs, tolerances", data_ivim_fit_saved())
163-
def test_ivim_fit_saved(name, bvals, data, algorithm, xfail, kwargs, tolerances, request):
59+
def test_ivim_fit_saved(name, bvals, data, algorithm, xfail, kwargs, tolerances, request, record_property):
16460
if xfail["xfail"]:
16561
mark = pytest.mark.xfail(reason="xfail", strict=xfail["strict"])
16662
request.node.add_marker(mark)
16763
fit = OsipiBase(algorithm=algorithm, **kwargs)
16864
signal, ratio = signal_helper(data["data"])
65+
16966
tolerances = tolerances_helper(tolerances, ratio, data["noise"])
17067
[f_fit, Dp_fit, D_fit] = fit.osipi_fit(signal, bvals)
68+
def to_list_if_needed(value):
69+
return value.tolist() if isinstance(value, np.ndarray) else value
70+
test_result = {
71+
"name": name,
72+
"algorithm": algorithm,
73+
"f_fit": to_list_if_needed(f_fit),
74+
"Dp_fit": to_list_if_needed(Dp_fit),
75+
"D_fit": to_list_if_needed(D_fit),
76+
"f": to_list_if_needed(data['f']),
77+
"Dp": to_list_if_needed(data['Dp']),
78+
"D": to_list_if_needed(data['D']),
79+
"rtol": tolerances["rtol"],
80+
"atol": tolerances["atol"]
81+
}
82+
83+
84+
record_property('test_data', test_result)
85+
17186
npt.assert_allclose(data['f'], f_fit, rtol=tolerances["rtol"]["f"], atol=tolerances["atol"]["f"])
87+
17288
if data['f']<0.80: # we need some signal for D to be detected
17389
npt.assert_allclose(data['D'], D_fit, rtol=tolerances["rtol"]["D"], atol=tolerances["atol"]["D"])
17490
if data['f']>0.03: #we need some f for D* to be interpretable
17591
npt.assert_allclose(data['Dp'], Dp_fit, rtol=tolerances["rtol"]["Dp"], atol=tolerances["atol"]["Dp"])
176-

utilities/report-summary.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import pathlib
2+
import json
3+
import sys
4+
5+
def summarize_test_report(input_file:str, output_file:str):
6+
file = pathlib.Path(__file__)
7+
report_path = file.with_name(input_file)
8+
with report_path.open() as f:
9+
report_info = json.load(f)
10+
summary = []
11+
for test_case in report_info['tests']:
12+
values = test_case['user_properties'][0]['test_data']
13+
values['status'] = test_case['outcome']
14+
summary.append(values)
15+
16+
with open(output_file, 'w') as f:
17+
json.dump(summary, f, indent=4)
18+
19+
if __name__ == '__main__':
20+
if len(sys.argv) != 3:
21+
print("Usage: python report-summary.py <input_file> <output_file>")
22+
sys.exit(1)
23+
24+
input_file = sys.argv[1]
25+
output_file = sys.argv[2]
26+
summarize_test_report(input_file, output_file)

website/dashboard/index.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
<script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.3.0/papaparse.min.js"></script>
1111
<script src="https://cdn.jsdelivr.net/npm/pako@1.0.11/dist/pako.min.js"></script>
1212
<script src="index.js"></script>
13+
<script src="test_plots.js"></script>
1314
<link rel="stylesheet" href="index.css">
1415
</head>
1516
<body>
@@ -97,6 +98,12 @@ <h1 class="bar-title">IVIM MRI Algorithm Fitting Dashboard</h1>
9798
<div class="chart-card" id="regionDiv">
9899
<!-- New chart will be rendered here -->
99100
</div>
101+
<h1>Validation Data Plots</h1>
102+
<div style="display: flex; flex-direction: column; gap: 1rem;">
103+
<div class="chart-card" id="plot_f_fit" "></div>
104+
<div class="chart-card" id="plot_Dp_fit" "></div>
105+
<div class="chart-card" id="plot_D_fit" "></div>
106+
</div>
100107

101108

102109
</main>

website/dashboard/test_plots.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
document.addEventListener('DOMContentLoaded', function() {
2+
fetch('report-summary.json')
3+
.then(response => response.json())
4+
.then(data => {
5+
function createPlot(container, parameter) {
6+
var reference_values = data.map(d => d[parameter]);
7+
var fit_values = data.map(d => d[parameter + '_fit']);
8+
var minRefValue = Math.min(...reference_values);
9+
var maxRefValue = Math.max(...reference_values);
10+
// Create a range for tolerance trace x axis.
11+
var xRange = [minRefValue, maxRefValue];
12+
// var DefaultTolerance = {
13+
// "rtol": {
14+
// "f": 0.05,
15+
// "D": 2,
16+
// "Dp": 0.5
17+
// },
18+
// "atol": {
19+
// "f": 0.2,
20+
// "D": 0.001,
21+
// "Dp": 0.06
22+
// }
23+
// }
24+
var DefaultTolerance = data[1] //Majority of the dataset has this tolerance values
25+
var tolerance = xRange.map((d) => DefaultTolerance['atol'][parameter] + DefaultTolerance['rtol'][parameter] * d);
26+
var negative_tolerance = tolerance.map(t => -t);
27+
28+
var errors = fit_values.map((d, i) => (d - reference_values[i]));
29+
30+
// Define colors for each status
31+
var statusColors = {
32+
'passed': 'green',
33+
'xfailed': 'blue',
34+
'failed': 'red'
35+
};
36+
37+
// Assign color based on the status
38+
var marker_colors = data.map(entry => statusColors[entry.status]);
39+
40+
var scatter_trace = {
41+
x: reference_values,
42+
y: errors,
43+
mode: 'markers',
44+
type: 'scatter',
45+
name: `${parameter} fitting values`,
46+
text: data.map(entry => `Algorithm: ${entry.algorithm} Region: ${entry.name}`),
47+
marker: {
48+
color: marker_colors
49+
}
50+
};
51+
52+
var tolerance_trace = {
53+
x: xRange,
54+
y: tolerance,
55+
type: 'scatter',
56+
mode: 'lines',
57+
line: { dash: 'dash', color: 'black' },
58+
name: 'Positive Tolerance'
59+
};
60+
61+
var negative_tolerance_trace = {
62+
x: xRange,
63+
y: negative_tolerance,
64+
type: 'scatter',
65+
mode: 'lines',
66+
line: { dash: 'dash', color: 'black' },
67+
name: 'Negative Tolerance'
68+
};
69+
70+
var layout = {
71+
title: `Error Plot for ${parameter.toUpperCase()}_fit with Tolerance Bands`,
72+
xaxis: { title: `Reference ${parameter.toUpperCase()} Values` },
73+
yaxis: { title: `Error (${parameter}_fit - Reference ${parameter})` }
74+
};
75+
76+
var plot_data = [scatter_trace, tolerance_trace, negative_tolerance_trace];
77+
78+
Plotly.newPlot(container, plot_data, layout);
79+
}
80+
81+
createPlot('plot_f_fit', 'f');
82+
createPlot('plot_Dp_fit', 'Dp');
83+
createPlot('plot_D_fit', 'D');
84+
});
85+
});

0 commit comments

Comments
 (0)