-
Notifications
You must be signed in to change notification settings - Fork 24
draft validation functions #90
Changes from 30 commits
dec80c1
566fc44
8bb2150
8aa73e7
9423015
1fcafa4
319e721
22d6c96
9402b66
598a0e1
96c8584
ffef504
92e94e6
29343dc
5995c43
cb9cc02
85419d8
dc4aebc
a59da92
f06c82f
dace6b3
9faf48f
777d309
b8377c5
8736701
b517507
26b9dcf
f7fa093
af84ec3
bd1f185
d0f81ab
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
"""Script to validate atlases""" | ||
|
||
|
||
from pathlib import Path | ||
|
||
import numpy as np | ||
from bg_atlasapi import BrainGlobeAtlas | ||
from bg_atlasapi.config import get_brainglobe_dir | ||
from bg_atlasapi.list_atlases import ( | ||
get_all_atlases_lastversions, | ||
get_atlases_lastversions, | ||
) | ||
from bg_atlasapi.update_atlases import update_atlas | ||
|
||
|
||
def validate_atlas_files(atlas_path: Path): | ||
"""Checks if basic files exist in the atlas folder""" | ||
|
||
assert atlas_path.is_dir(), f"Atlas path {atlas_path} not found" | ||
expected_files = [ | ||
"annotation.tiff", | ||
"reference.tiff", | ||
"metadata.json", | ||
"structures.json", | ||
] | ||
for expected_file_name in expected_files: | ||
expected_path = Path(atlas_path / expected_file_name) | ||
assert ( | ||
expected_path.is_file() | ||
), f"Expected file not found at {expected_path}" | ||
|
||
meshes_path = atlas_path / "meshes" | ||
assert meshes_path.is_dir(), f"Meshes path {meshes_path} not found" | ||
return True | ||
|
||
|
||
def _assert_close(mesh_coord, annotation_coord, pixel_size, diff_tolerance=10): | ||
""" | ||
Helper function to check if the mesh and the annotation coordinate | ||
are closer to each other than an arbitrary tolerance value times the pixel size. | ||
The default tolerance value is 10. | ||
""" | ||
assert abs(mesh_coord - annotation_coord) <= diff_tolerance * pixel_size, ( | ||
f"Mesh coordinate {mesh_coord} and annotation coordinate {annotation_coord}", | ||
f"differ by more than {diff_tolerance} times pixel size {pixel_size}", | ||
) | ||
return True | ||
|
||
|
||
def validate_mesh_matches_image_extents(atlas: BrainGlobeAtlas): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function is longish, with some repetitions. It could use some further refactoring imo. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For example, you could extract some bits as further helper functions (the way it's done for |
||
"""Checks if the mesh and the image extents are similar""" | ||
|
||
root_mesh = atlas.mesh_from_structure("root") | ||
annotation_image = atlas.annotation | ||
resolution = atlas.resolution | ||
|
||
# minimum and maximum values of the annotation image (z, y, x) | ||
z_range, y_range, x_range = np.nonzero(annotation_image) | ||
z_min, z_max = np.min(z_range), np.max(z_range) | ||
y_min, y_max = np.min(y_range), np.max(y_range) | ||
x_min, x_max = np.min(x_range), np.max(x_range) | ||
|
||
# minimum and maximum values of the annotation image scaled by the atlas resolution | ||
z_min_scaled, z_max_scaled = z_min * resolution[0], z_max * resolution[0] | ||
y_min_scaled, y_max_scaled = y_min * resolution[1], y_max * resolution[1] | ||
x_min_scaled, x_max_scaled = x_min * resolution[2], x_max * resolution[2] | ||
|
||
# z, y and x coordinates of the root mesh (extent of the whole object) | ||
mesh_points = root_mesh.points | ||
z_coords, y_coords, x_coords = ( | ||
mesh_points[:, 0], | ||
mesh_points[:, 1], | ||
mesh_points[:, 2], | ||
) | ||
|
||
# minimum and maximum coordinates of the root mesh | ||
z_min_mesh, z_max_mesh = np.min(z_coords), np.max(z_coords) | ||
y_min_mesh, y_max_mesh = np.min(y_coords), np.max(y_coords) | ||
x_min_mesh, x_max_mesh = np.min(x_coords), np.max(x_coords) | ||
|
||
# checking if root mesh and image are on the same scale | ||
_assert_close(z_min_mesh, z_min_scaled, resolution[0]) | ||
_assert_close(z_max_mesh, z_max_scaled, resolution[0]) | ||
_assert_close(y_min_mesh, y_min_scaled, resolution[1]) | ||
_assert_close(y_max_mesh, y_max_scaled, resolution[1]) | ||
_assert_close(x_min_mesh, x_min_scaled, resolution[2]) | ||
_assert_close(x_max_mesh, x_max_scaled, resolution[2]) | ||
|
||
return True | ||
|
||
|
||
def open_for_visual_check(): | ||
pass | ||
|
||
|
||
def validate_checksum(): | ||
pass | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sonar lint complains because it requires you to add a comment to such "empty" functions, mentioning what you intend these for (like you have done for the following |
||
|
||
|
||
def check_additional_references(): | ||
# check additional references are different, but have same dimensions | ||
pass | ||
|
||
|
||
def validate_atlas(atlas_name, version, all_validation_functions): | ||
"""Validates the latest version of a given atlas""" | ||
|
||
print(atlas_name, version) | ||
BrainGlobeAtlas(atlas_name) | ||
updated = get_atlases_lastversions()[atlas_name]["updated"] | ||
if not updated: | ||
update_atlas(atlas_name) | ||
Path(get_brainglobe_dir()) / f"{atlas_name}_v{version}" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this line can be removed, as far as I can see it's not being assigned to avariable to used in any other way. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In fact, the removal of this line is probably necessary for the Sonar Lint tests to pass. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
|
||
validation_function_parameters = [ | ||
# validate_atlas_files(atlas_path: Path) | ||
(Path(get_brainglobe_dir() / f"{atlas_name}_v{version}"),), | ||
# validate_mesh_matches_image_extents(atlas: BrainGlobeAtlas) | ||
(BrainGlobeAtlas(atlas_name),), | ||
# open_for_visual_check() | ||
(), | ||
# validate_checksum() | ||
(), | ||
# check_additional_references() | ||
(), | ||
# validate_atlas(atlas_name, version) | ||
(atlas_name, version), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In total I count 5 validation functions being passed, what's this 6th set of parameters for? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It was left there by mistake. Removed now |
||
] | ||
|
||
# list to store the errors of the failed validations | ||
failed_validations = [] | ||
successful_validations = [] | ||
|
||
for i, validation_function in enumerate(all_validation_functions): | ||
try: | ||
validation_function(*validation_function_parameters[i]) | ||
successful_validations.append((atlas_name, validation_function)) | ||
except AssertionError as error: | ||
failed_validations.append((atlas_name, validation_function, error)) | ||
|
||
return successful_validations, failed_validations | ||
|
||
|
||
if __name__ == "__main__": | ||
# list to store the validation functions | ||
all_validation_functions = [ | ||
validate_atlas_files, | ||
validate_mesh_matches_image_extents, | ||
open_for_visual_check, | ||
validate_checksum, | ||
check_additional_references, | ||
] | ||
|
||
valid_atlases = [] | ||
invalid_atlases = [] | ||
for atlas_name, version in get_all_atlases_lastversions().items(): | ||
successful_validations, failed_validations = validate_atlas( | ||
atlas_name, version, all_validation_functions | ||
) | ||
for item in successful_validations: | ||
valid_atlases.append(item) | ||
for item in failed_validations: | ||
invalid_atlases.append(item) | ||
|
||
print("Summary") | ||
print("### Valid atlases ###") | ||
print(valid_atlases) | ||
print("### Invalid atlases ###") | ||
print(invalid_atlases) | ||
Comment on lines
+164
to
+168
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it be useful to also save the output in a .txt (or .md) file in addition to printing it? |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
from pathlib import Path | ||
|
||
import numpy as np | ||
import pytest | ||
from bg_atlasapi import BrainGlobeAtlas | ||
from bg_atlasapi.config import get_brainglobe_dir | ||
|
||
from bg_atlasgen.validate_atlases import ( | ||
_assert_close, | ||
validate_atlas_files, | ||
validate_mesh_matches_image_extents, | ||
) | ||
|
||
|
||
def test_validate_mesh_matches_image_extents(): | ||
atlas = BrainGlobeAtlas("allen_mouse_100um") | ||
assert validate_mesh_matches_image_extents(atlas) | ||
|
||
|
||
def test_validate_mesh_matches_image_extents_negative(mocker): | ||
atlas = BrainGlobeAtlas("allen_mouse_100um") | ||
flipped_annotation_image = np.transpose(atlas.annotation) | ||
mocker.patch( | ||
"bg_atlasapi.BrainGlobeAtlas.annotation", | ||
new_callable=mocker.PropertyMock, | ||
return_value=flipped_annotation_image, | ||
) | ||
with pytest.raises( | ||
AssertionError, match="differ by more than 10 times pixel size" | ||
): | ||
validate_mesh_matches_image_extents(atlas) | ||
|
||
|
||
def test_valid_atlas_files(): | ||
_ = BrainGlobeAtlas("allen_mouse_100um") | ||
atlas_path = Path(get_brainglobe_dir()) / "allen_mouse_100um_v1.2" | ||
assert validate_atlas_files(atlas_path) | ||
|
||
|
||
def test_invalid_atlas_path(): | ||
atlas_path = Path.home() | ||
with pytest.raises(AssertionError, match="Expected file not found"): | ||
validate_atlas_files(atlas_path) | ||
|
||
|
||
def test_assert_close(): | ||
assert _assert_close(99.5, 8, 10) | ||
|
||
|
||
def test_assert_close_negative(): | ||
with pytest.raises( | ||
AssertionError, match="differ by more than 10 times pixel size" | ||
): | ||
_assert_close(99.5, 30, 2) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @viktorpm, the test failure is caused by this assertion. The problem is that you are checking if all elements in
expected_files
are indeed existing files (as I suggested), but "meshes" is a folder not a file. I would check the meshes separately withis_dir()
, for example:It's important that you check the meshes folder after you check individual files, otherwise the
test_invalid_atlas_path()
will fail.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you! It's fixed now