Skip to content

Commit ad6f644

Browse files
authored
Merge pull request #11 from psavery/separate-pyxrf-out
Allow pyxrf to be ran in its own environment
2 parents 1022c70 + d98275b commit ad6f644

58 files changed

Lines changed: 1765 additions & 785 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/build_and_test.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ jobs:
6565
conda install -y --override-channels -c conda-forge --file tomviz/tests/python/requirements-dev.txt
6666
pip install --no-build-isolation --no-deps -U tomviz/tomviz/python
6767
pip install --no-build-isolation --no-deps -U tomviz/acquisition
68+
# These do not have to be in the same environment as Tomviz, but the dependencies
69+
# are so far compatible and we'll just let it go for the tests.
70+
conda install -y --override-channels -c conda-forge --file tomviz/.github/workflows/pyxrf_requirements.txt
71+
pip install --no-build-isolation --no-deps -U tomviz/tomviz/python/tomviz/pyxrf/pyxrf-utils
6872
6973
- name: Run Tests
7074
env:

.github/workflows/build_requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ vtk
66
libxml2-devel=2.14*
77
# This is needed for building the tests
88
gtest
9+
# This is needed for installing stuff on Python3.13
10+
setuptools
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# These are PyXRF workflow dependencies
2+
hxntools
3+
pyxrf
4+
xrf-tomo
5+
xraylib
6+
scikit-beam
7+
tomopy
8+
# These are required for installing the pyxrf-utils environment
9+
hatchling

.github/workflows/runtime_requirements.txt

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,4 @@ jsonpointer
77
pyfftw
88
pygments
99
pystackreg
10-
# These are PyXRF workflow dependencies
1110
h5py
12-
hxntools
13-
pyxrf
14-
xrf-tomo
15-
xraylib
16-
scikit-beam
17-
tomopy

pixi.toml

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,8 @@ platforms = ["linux-64"]
99
python = "==3.12"
1010
tomviz = ">=2.1.3"
1111
h5py = "*"
12-
xraylib = "*"
1312
tomopy = "*"
1413

15-
[pypi-dependencies]
16-
bluesky-tiled-plugins = ">=2.0.0rc1"
17-
pyxrf = "*"
18-
hxntools = "*"
19-
xrf-tomo = "*"
20-
scikit-beam = "*"
21-
tiled = { version = ">=0.2.0", extras = ["client"] }
22-
2314
[tasks]
2415
# Launch Tomviz from the pixi environment
2516
tomviz = "tomviz"

tests/cxx/PyXRFWorkflowTest.cxx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,12 @@ private slots:
130130
// After accepting, a modal dialog will appear. Start posting
131131
// events to check it and accept it when it appears.
132132
bool found = false;
133-
auto checkFunc = [&found](){
133+
134+
std::function<void()> checkFunc;
135+
checkFunc = [&found, &checkFunc](){
134136
auto* dialog = findWidget<SelectItemsDialog>();
135137
if (!dialog) {
138+
QTimer::singleShot(1000, checkFunc);
136139
return;
137140
}
138141

@@ -150,7 +153,6 @@ private slots:
150153
int maxTime = 30;
151154
while (!found && timeElapsed < maxTime) {
152155
QThread::sleep(1);
153-
QTimer::singleShot(0, checkFunc);
154156
QApplication::processEvents();
155157
timeElapsed += 1;
156158
}

tests/python/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,7 @@ include(PythonTests.cmake)
33
add_python_test(operator)
44
add_python_test(external)
55
add_python_test(pystackreg)
6+
add_python_test(multi_array)
7+
add_python_test(xcorr)
8+
add_python_test(tilt_axis_shift)
9+
add_python_test(constraint_dft)

tests/python/conftest.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99

1010
import numpy as np
1111

12+
from tomviz.executor import load_dataset
13+
from tomviz.external_dataset import Dataset
14+
1215
from utils import download_file, download_and_unzip_file
1316

1417
DATA_URL = 'https://data.kitware.com/api/v1/file'
@@ -30,6 +33,25 @@ def hxn_xrf_example_output_dir(data_dir: Path) -> Path:
3033
return output_dir
3134

3235

36+
@pytest.fixture(scope='function')
37+
def hxn_xrf_example_dataset(hxn_xrf_example_output_dir: Path) -> Dataset:
38+
example_files = [
39+
'Pt_L.h5',
40+
'Zn_K.h5',
41+
]
42+
example_files = [
43+
hxn_xrf_example_output_dir / 'extracted_elements' / name
44+
for name in example_files
45+
]
46+
dataset = load_dataset(example_files[0])
47+
for new_file in example_files[1:]:
48+
new_dataset = load_dataset(new_file)
49+
new_name = new_dataset.active_name
50+
dataset.arrays[new_name] = new_dataset.active_scalars
51+
52+
return dataset
53+
54+
3355
@pytest.fixture
3456
def pystackreg_reference_output(data_dir: Path) -> dict[str, np.ndarray]:
3557
filepath = data_dir / 'test_pystackreg_reference_output.npz'
@@ -41,6 +63,39 @@ def pystackreg_reference_output(data_dir: Path) -> dict[str, np.ndarray]:
4163
return np.load(filepath)
4264

4365

66+
@pytest.fixture
67+
def xcorr_reference_output(data_dir: Path) -> dict[str, np.ndarray]:
68+
filepath = data_dir / 'test_xcorr_reference_output.npz'
69+
if not filepath.exists():
70+
# Download it
71+
url = DATA_URL + '/6920b212cdb169d7a021d769/download'
72+
download_file(url, filepath)
73+
74+
return np.load(filepath)
75+
76+
77+
@pytest.fixture
78+
def tilt_axis_shift_reference_output(data_dir: Path) -> dict[str, np.ndarray]:
79+
filepath = data_dir / 'test_tilt_axis_shift_reference_output.npz'
80+
if not filepath.exists():
81+
# Download it
82+
url = DATA_URL + '/6920b333cdb169d7a021d76c/download'
83+
download_file(url, filepath)
84+
85+
return np.load(filepath)
86+
87+
88+
@pytest.fixture
89+
def constraint_dft_reference_output(data_dir: Path) -> dict[str, np.ndarray]:
90+
filepath = data_dir / 'test_constraint_dft_reference_output.npz'
91+
if not filepath.exists():
92+
# Download it
93+
url = DATA_URL + '/6920b750cdb169d7a021d773/download'
94+
download_file(url, filepath)
95+
96+
return np.load(filepath)
97+
98+
4499
@pytest.fixture(scope="module")
45100
def test_state_file(tmpdir_factory):
46101
tmpdir = tmpdir_factory.mktemp('state')
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import numpy as np
2+
3+
from utils import load_operator_class, load_operator_module
4+
5+
from tomviz.external_dataset import Dataset
6+
7+
8+
def test_constraint_dft(hxn_xrf_example_dataset: Dataset,
9+
constraint_dft_reference_output: dict[str, np.ndarray]):
10+
dataset = hxn_xrf_example_dataset
11+
reference_output = constraint_dft_reference_output
12+
13+
# Load the operator module
14+
module = load_operator_module('Recon_DFT_constraint')
15+
operator = load_operator_class(module)
16+
17+
# Verify the keys match
18+
assert list(sorted(reference_output)) == list(sorted(dataset.arrays))
19+
20+
# But the arrays should not match. In fact, the shapes should not match.
21+
assert not all(
22+
np.array_equal(reference_output[key].shape, dataset.arrays[key].shape)
23+
for key in reference_output
24+
)
25+
26+
# Apply the transformation
27+
results = operator.transform(
28+
dataset,
29+
Niter=10,
30+
Niter_update_support=50,
31+
supportSigma=0.1,
32+
supportThreshold=10,
33+
seed=0,
34+
)
35+
36+
recon_dataset = results['reconstruction']
37+
38+
# Verify the keys match
39+
assert list(sorted(reference_output)) == list(sorted(recon_dataset.arrays))
40+
41+
# Arrays should now match
42+
assert all(
43+
np.allclose(reference_output[key], recon_dataset.arrays[key], atol=1e-2)
44+
for key in reference_output
45+
)
46+
47+
# Save a new reference output:
48+
# np.savez_compressed('test_constraint_dft_reference_output.npz', **recon_dataset.arrays)

tests/python/multi_array_test.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import numpy as np
2+
3+
from utils import load_operator_class, load_operator_module
4+
5+
from tomviz.external_dataset import Dataset
6+
7+
8+
def test_multi_arrays(hxn_xrf_example_dataset: Dataset):
9+
dataset = hxn_xrf_example_dataset
10+
11+
# Test a few operators that use `@apply_to_each_array`, and verify
12+
# that they do indeed apply to each array.
13+
# Load the operator module
14+
bin_module = load_operator_module('BinTiltSeriesByTwo')
15+
16+
# Verify the tilt angles won't change, but image shape decreases by 2x
17+
orig_shapes = {k: v.shape for k, v in dataset.arrays.items()}
18+
orig_tilt_angles = dataset.tilt_angles
19+
20+
# Apply the transformation
21+
bin_module.transform(dataset)
22+
23+
# New shapes should be binned by 2 in x and y, but not z
24+
# Tilt angles should be unaffected
25+
binned_shapes = {k: v.shape for k, v in dataset.arrays.items()}
26+
expected_shapes = {k: (shape[0] // 2, shape[1] // 2, shape[2])
27+
for k, shape in orig_shapes.items()}
28+
29+
assert binned_shapes == expected_shapes
30+
assert np.allclose(orig_tilt_angles, dataset.tilt_angles)
31+
32+
# Delete 3 slices. This will delete tilt angles too.
33+
delete_slices_module = load_operator_module('DeleteSlices')
34+
35+
first_slice = 3
36+
last_slice = 5
37+
num_deleted = last_slice - first_slice + 1
38+
delete_slices_module.transform(dataset,
39+
firstSlice=first_slice,
40+
lastSlice=last_slice,
41+
axis=2)
42+
43+
# New shapes should be the same, other than 3 less in Z
44+
expected_shapes = {k: (shape[0], shape[1], shape[2] - num_deleted)
45+
for k, shape in binned_shapes.items()}
46+
deleted_shapes = {k: v.shape for k, v in dataset.arrays.items()}
47+
48+
expected_angles = np.hstack((orig_tilt_angles[:first_slice],
49+
orig_tilt_angles[last_slice + 1:]))
50+
51+
assert deleted_shapes == expected_shapes
52+
assert np.allclose(dataset.tilt_angles, expected_angles)
53+
54+
# Now run a reconstruction and verify we get all results
55+
recon_sirt_module = load_operator_module('Recon_SIRT')
56+
operator = load_operator_class(recon_sirt_module)
57+
58+
results = operator.transform(dataset, Niter=2)
59+
60+
recon_dataset = results['reconstruction']
61+
62+
# We should have the same scalar names
63+
assert sorted(recon_dataset.scalars_names) == sorted(dataset.scalars_names)
64+
65+
# Verify the output appears valid. We are not testing the actual
66+
# reconstruction operator here, just that it ran on multiple arrays.
67+
means = [np.mean(array) for array in recon_dataset.arrays.values()]
68+
assert np.all(np.array(means) > 0)

0 commit comments

Comments
 (0)