diff --git a/afids_utils/afids.py b/afids_utils/afids.py index 4f8f968d..e9daac80 100644 --- a/afids_utils/afids.py +++ b/afids_utils/afids.py @@ -178,8 +178,12 @@ def load(cls, afids_fpath: PathLike[str] | str) -> AfidSet: afids_fpath ) # Loading json - # if afids_fpath_ext = ".json": - # load_json(afids_path) + elif afids_fpath_ext == ".json": + from afids_utils.ext.json import load_json + + slicer_version, coord_system, afids_positions = load_json( + afids_fpath + ) else: raise ValueError("Unsupported file extension") @@ -229,8 +233,10 @@ def save(self, out_fpath: PathLike[str] | str) -> None: save_fcsv(self, out_fpath) # Saving json - # if out_fpath_ext = ".json": - # save_json(afids_coords, out_fpath) + elif out_fpath_ext == ".json": + from afids_utils.ext.json import save_json + + save_json(self, out_fpath) else: raise ValueError("Unsupported file extension") diff --git a/afids_utils/ext/fcsv.py b/afids_utils/ext/fcsv.py index a813a4d0..78779bf0 100644 --- a/afids_utils/ext/fcsv.py +++ b/afids_utils/ext/fcsv.py @@ -29,8 +29,7 @@ def _get_metadata(in_fcsv: list[str]) -> tuple[str, str]: - """ - Internal function to extract metadata from header of fcsv files + """Internal function to extract metadata from header of fcsv files Parameters ---------- @@ -73,8 +72,7 @@ def _get_metadata(in_fcsv: list[str]) -> tuple[str, str]: def _get_afids(in_fcsv: list[str]) -> list[AfidPosition]: - """ - Internal function for converting .fcsv file to a pl.DataFrame + """Internal function for grabbing AFID positions from .fcsv file Parameters ---------- @@ -109,8 +107,7 @@ def _get_afids(in_fcsv: list[str]) -> list[AfidPosition]: def load_fcsv( fcsv_path: PathLike[str] | str, ) -> tuple[str, str, list[AfidPosition]]: - """ - Read in fcsv to an AfidSet + """Read in fcsv and extract relevant information for an AfidSet Parameters ---------- @@ -143,8 +140,7 @@ def save_fcsv( afid_set: AfidSet, out_fcsv: PathLike[str] | str, ) -> None: - """ - Save fiducials to output fcsv file + """Save fiducials to output fcsv file Parameters ---------- @@ -153,11 +149,6 @@ def save_fcsv( out_fcsv Path of fcsv file to save AFIDs to - - Raises - ------ - TypeError - If number of fiducials to write does not match expected number """ # Read in fcsv template with resources.open_text( diff --git a/afids_utils/ext/json.py b/afids_utils/ext/json.py new file mode 100644 index 00000000..241c2b8f --- /dev/null +++ b/afids_utils/ext/json.py @@ -0,0 +1,163 @@ +"""Methods for handling .json files associated with AFIDS""" +from __future__ import annotations + +import json +from importlib import resources +from os import PathLike + +from typing_extensions import TypedDict + +from afids_utils.afids import AfidPosition, AfidSet +from afids_utils.exceptions import InvalidFileError + + +class ControlPoint(TypedDict): + id: str + label: str + description: str + associatedNodeID: str + position: list[float] + orientation: list[float] + selected: bool + locked: bool + visibility: bool + positionStatus: str + + +def _get_metadata(coord_system: str) -> tuple[str, str]: + """Internal function to extract metadata from json files + + Note: Slicer version is not currently included in the json file + + Parameters: + ----------- + coord_system + Coordinate system parsed from json_file + + Returns + ------- + parsed_version + Slicer version associated with fiducial file + + parsed_coord + Coordinate system of fiducials + + Raises + ------ + InvalidFileError + If header is invalid from .json file + """ + + # Update if future json versions include Slicer version + parsed_version = "Unknown" + parsed_coord = coord_system + + # Transform coordinate system so human-understandable + if parsed_coord == "0": + parsed_coord = "RAS" + elif parsed_coord == "1": + parsed_coord = "LPS" + + if parsed_coord not in ["RAS", "LPS"]: + raise InvalidFileError("Invalid coordinate system") + + return parsed_version, parsed_coord + + +def _get_afids(control_points: list[ControlPoint]) -> list[AfidPosition]: + """Internal function to parse fiducial information from json file + + Parameters + ---------- + ctrl_points + List of dicts containing fiducial information from parsed json file + + Returns + ------- + afid_positions + List containing spatial position of afids + """ + afids_positions = [ + AfidPosition( + label=int(afid["label"]), + x=float(afid["position"][0]), + y=float(afid["position"][1]), + z=float(afid["position"][2]), + desc=afid["description"], + ) + for afid in control_points + ] + + return afids_positions + + +def load_json( + json_path: PathLike[str] | str, +) -> tuple[str, str, list[AfidPosition]]: + """Read in json and extract relevant information for an AfidSet + + Parameters + ---------- + json_path + Path to .json file containing AFIDs coordinates + + Returns + ------- + slicer_version + Slicer version associated with fiducial file + + coord_system + Coordinate system of fiducials + + afids_positions + List containing spatial position of afids + """ + with open(json_path) as json_file: + afids_json = json.load(json_file) + + # Grab metadata + slicer_version, coord_system = _get_metadata( + afids_json["markups"][0]["coordinateSystem"] + ) + # Grab afids + afids_positions = _get_afids(afids_json["markups"][0]["controlPoints"]) + + return slicer_version, coord_system, afids_positions + + +def save_json( + afid_set: AfidSet, + out_json: PathLike[str] | str, +) -> None: + """Save fiducials to output json file + + Parameters + ---------- + afid_set + A complete AfidSet containing metadata and positions of AFIDs + + out_json + Path of json file to save AFIDs to + """ + # Read in json template + with resources.open_text( + "afids_utils.resources", "template.json" + ) as template_json_file: + template_content = json.load(template_json_file) + + # Update header + template_content["markups"][0][ + "coordinateSystem" + ] = afid_set.coord_system + + # Loop and update with fiducial coordinates + for idx in range(len(template_content["markups"][0]["controlPoints"])): + template_content["markups"][0]["controlPoints"][idx]["position"] = [ + afid_set.afids[idx].x, + afid_set.afids[idx].y, + afid_set.afids[idx].z, + ] + + # Write output json + with open(out_json, "w") as out_json_file: + json.dump(template_content, out_json_file, indent=4) diff --git a/afids_utils/resources/template.json b/afids_utils/resources/template.json new file mode 100644 index 00000000..66a46e1a --- /dev/null +++ b/afids_utils/resources/template.json @@ -0,0 +1,422 @@ +{ + "@schema": "https://raw.githubusercontent.com/slicer/slicer/master/Modules/Loadable/Markups/Resources/Schema/markups-schema-v1.0.0.json#", + "markups": [ + { + "type": "Fiducial", + "coordinateSystem": "RAS", + "locked": false, + "labelFormat": "%N-%d", + "controlPoints": [ + { + "id": "vtkMRMLMarkupsFiducialNode_1", + "label": "1", + "description": "AC", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [1.0, 1.0, 1.0], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_2", + "label": "2", + "description": "PC", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [1.0, 1.0, 1.0], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_3", + "label": "3", + "description": "infracollicular sulcus", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [1.0, 1.0, 1.0], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_4", + "label": "4", + "description": "PMJ", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [1.0, 1.0, 1.0], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_5", + "label": "5", + "description": "superior interpeduncular fossa", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [1.0, 1.0, 1.0], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_6", + "label": "6", + "description": "R superior LMS", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [1.0, 1.0, 1.0], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_7", + "label": "7", + "description": "L superior LMS", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [1.0, 1.0, 1.0], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_8", + "label": "8", + "description": "R inferior LMS", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [1.0, 1.0, 1.0], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_9", + "label": "9", + "description": "L inferior LMS", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [1.0, 1.0, 1.0], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_10", + "label": "10", + "description": "Culmen", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [1.0, 1.0, 1.0], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_11", + "label": "11", + "description": "Intermammillary sulcus", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [1.0, 1.0, 1.0], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_12", + "label": "12", + "description": "R MB", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [1.0, 1.0, 1.0], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_13", + "label": "13", + "description": "L MB", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [1.0, 1.0, 1.0], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_14", + "label": "14", + "description": "pineal gland", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [1.0, 1.0, 1.0], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_15", + "label": "15", + "description": "R LV at AC", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [1.0, 1.0, 1.0], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_16", + "label": "16", + "description": "L LV at AC", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [1.0, 1.0, 1.0], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_17", + "label": "17", + "description": "R LV at PC", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [1.0, 1.0, 1.0], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_18", + "label": "18", + "description": "L LV at PC", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [1.0, 1.0, 1.0], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_19", + "label": "19", + "description": "Genu of CC", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [1.0, 1.0, 1.0], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_20", + "label": "20", + "description": "Splenium of CC", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [1.0, 1.0, 1.0], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_21", + "label": "21", + "description": "R AL temporal horn", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [1.0, 1.0, 1.0], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_22", + "label": "22", + "description": "L AL temporal horn", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [1.0, 1.0, 1.0], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_23", + "label": "23", + "description": "R superior AM temporal horn", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [1.0, 1.0, 1.0], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_24", + "label": "24", + "description": "L superior AM temporal horn", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [1.0, 1.0, 1.0], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_25", + "label": "25", + "description": "R inferior AM temporal horn", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [1.0, 1.0, 1.0], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_26", + "label": "26", + "description": "L inferior AM temporal horn", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [1.0, 1.0, 1.0], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_27", + "label": "27", + "description": "R indusium griseum origin", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [1.0, 1.0, 1.0], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_28", + "label": "28", + "description": "L indusium griseum origin", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [1.0, 1.0, 1.0], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_29", + "label": "29", + "description": "R ventral occipital horn", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [1.0, 1.0, 1.0], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_30", + "label": "30", + "description": "L ventral occipital horn", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [1.0, 1.0, 1.0], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_31", + "label": "31", + "description": "R olfactory sulcal fundus", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [1.0, 1.0, 1.0], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_32", + "label": "32", + "description": "L olfactory sulcal fundus", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [1.0, 1.0, 1.0], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + } + ], + "display": { + "visibility": true, + "opacity": 1.0, + "color": [0.4, 1.0, 1.0], + "selectedColor": [1.0, 0.5000076295109484, 0.5000076295109484], + "propertiesLabelVisibility": false, + "pointLabelsVisibility": true, + "textScale": 3.0, + "glyphType": "Sphere3D", + "glyphScale": 1.0, + "glyphSize": 5.0, + "useGlyphScale": true, + "sliceProjection": false, + "sliceProjectionUseFiducialColor": true, + "sliceProjectionOutlinedBehindSlicePlane": false, + "sliceProjectionColor": [1.0, 1.0, 1.0], + "sliceProjectionOpacity": 0.6, + "lineThickness": 0.2, + "lineColorFadingStart": 1.0, + "lineColorFadingEnd": 10.0, + "lineColorFadingSaturation": 1.0, + "lineColorFadingHueOffset": 0.0, + "handlesInteractive": false, + "snapMode": "toVisibleSurface" + } + } + ] +} diff --git a/afids_utils/tests/data/tpl-MNI152NLin2009cAsym_afids.json b/afids_utils/tests/data/tpl-MNI152NLin2009cAsym_afids.json new file mode 100644 index 00000000..2fb827a5 --- /dev/null +++ b/afids_utils/tests/data/tpl-MNI152NLin2009cAsym_afids.json @@ -0,0 +1,422 @@ +{ + "@schema": "https://raw.githubusercontent.com/slicer/slicer/master/Modules/Loadable/Markups/Resources/Schema/markups-schema-v1.0.0.json#", + "markups": [ + { + "type": "Fiducial", + "coordinateSystem": "RAS", + "locked": false, + "labelFormat": "%N-%d", + "controlPoints": [ + { + "id": "vtkMRMLMarkupsFiducialNode_1", + "label": "1", + "description": "AC", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [-0.23099999999999998, 2.93275, -4.899], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_2", + "label": "2", + "description": "PC", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [-0.24125000000000002, -25.074749999999998, -2.169], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_3", + "label": "3", + "description": "infracollicular sulcus", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [0.19949999999999998, -37.268249999999995, -10.9115], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_4", + "label": "4", + "description": "PMJ", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [-0.12725000000000003, -23.14825, -21.522], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_5", + "label": "5", + "description": "superior interpeduncular fossa", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [-0.06675, -14.81275, -11.32025], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_6", + "label": "6", + "description": "R superior LMS", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [12.67275, -26.960749999999997, -10.38225], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_7", + "label": "7", + "description": "L superior LMS", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [-13.004999999999999, -27.190749999999998, -10.32375], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_8", + "label": "8", + "description": "R inferior LMS", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [10.873, -30.96475, -21.533], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_9", + "label": "9", + "description": "L inferior LMS", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [-11.18, -30.437000000000005, -21.537], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_10", + "label": "10", + "description": "Culmen", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [-0.004750000000000004, -52.3695, 2.06825], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_11", + "label": "11", + "description": "Intermammillary sulcus", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [-0.0665, -8.131499999999999, -14.7755], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_12", + "label": "12", + "description": "R MB", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [2.0255, -8.143500000000001, -14.752749999999999], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_13", + "label": "13", + "description": "L MB", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [-2.3954999999999997, -8.0565, -14.8675], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_14", + "label": "14", + "description": "pineal gland", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [0.04425, -31.73675, 0.76675], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_15", + "label": "15", + "description": "R LV at AC", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [15.141750000000002, 5.3445, 24.358249999999998], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_16", + "label": "16", + "description": "L LV at AC", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [-15.6095, 5.22575, 24.63125], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_17", + "label": "17", + "description": "R LV at PC", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [18.4955, -22.061999999999998, 27.66], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_18", + "label": "18", + "description": "L LV at PC", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [-18.999000000000002, -22.046, 27.284], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_19", + "label": "19", + "description": "Genu of CC", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [0.17799999999999996, 33.423500000000004, 2.6755], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_20", + "label": "20", + "description": "Splenium of CC", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [0.037000000000000005, -37.6845, 6.14675], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_21", + "label": "21", + "description": "R AL temporal horn", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [34.312250000000006, -4.982, -26.87875], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_22", + "label": "22", + "description": "L AL temporal horn", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [-34.72725, -7.2475, -25.203], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_23", + "label": "23", + "description": "R superior AM temporal horn", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [18.69625, -10.56775, -17.7725], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_24", + "label": "24", + "description": "L superior AM temporal horn", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [-20.12075, -11.62125, -17.1495], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_25", + "label": "25", + "description": "R inferior AM temporal horn", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [21.07325, -4.56975, -28.90925], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_26", + "label": "26", + "description": "L inferior AM temporal horn", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [-21.966749999999998, -5.28875, -27.895], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_27", + "label": "27", + "description": "R indusium griseum origin", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [14.40775, -39.697250000000004, 3.8390000000000004], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_28", + "label": "28", + "description": "L indusium griseum origin", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [-15.48075, -42.28375, 3.9659999999999997], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_29", + "label": "29", + "description": "R ventral occipital horn", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [20.730249999999998, -79.47475, 4.85125], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_30", + "label": "30", + "description": "L ventral occipital horn", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [-19.41825, -81.46925, 3.785], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_31", + "label": "31", + "description": "R olfactory sulcal fundus", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [12.108500000000001, 17.02725, -12.94], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + }, + { + "id": "vtkMRMLMarkupsFiducialNode_32", + "label": "32", + "description": "L olfactory sulcal fundus", + "associatedNodeID": "vtkMRMLScalarVolumeNode1", + "position": [-13.34325, 17.0885, -13.35225], + "orientation": [-1.0, -0.0, -0.0, -0.0, -1.0, -0.0, 0.0, 0.0, 1.0], + "selected": true, + "locked": false, + "visibility": true, + "positionStatus": "defined" + } + ], + "display": { + "visibility": true, + "opacity": 1.0, + "color": [0.4, 1.0, 1.0], + "selectedColor": [1.0, 0.5000076295109484, 0.5000076295109484], + "propertiesLabelVisibility": false, + "pointLabelsVisibility": true, + "textScale": 3.0, + "glyphType": "Sphere3D", + "glyphScale": 1.0, + "glyphSize": 5.0, + "useGlyphScale": true, + "sliceProjection": false, + "sliceProjectionUseFiducialColor": true, + "sliceProjectionOutlinedBehindSlicePlane": false, + "sliceProjectionColor": [1.0, 1.0, 1.0], + "sliceProjectionOpacity": 0.6, + "lineThickness": 0.2, + "lineColorFadingStart": 1.0, + "lineColorFadingEnd": 10.0, + "lineColorFadingSaturation": 1.0, + "lineColorFadingHueOffset": 0.0, + "handlesInteractive": false, + "snapMode": "toVisibleSurface" + } + } + ] +} diff --git a/afids_utils/tests/test_afids.py b/afids_utils/tests/test_afids.py index a8d538e7..20d92582 100644 --- a/afids_utils/tests/test_afids.py +++ b/afids_utils/tests/test_afids.py @@ -18,7 +18,7 @@ @pytest.fixture -def valid_fcsv_file() -> PathLike[str]: +def valid_file() -> Path: return ( Path(__file__).parent / "data" / "tpl-MNI152NLin2009cAsym_afids.fcsv" ) @@ -158,17 +158,16 @@ def test_repeated_incomplete_afid_set( class TestAfidsIO: - @given(label=st.integers(min_value=0, max_value=31)) + @given( + label=st.integers(min_value=0, max_value=31), + ext=st.sampled_from(["fcsv", "json"]), + ) @settings( suppress_health_check=[HealthCheck.function_scoped_fixture], ) - def test_valid_load( - self, - valid_fcsv_file: PathLike[str], - label: int, - ): + def test_valid_load(self, valid_file: PathLike[str], label: int, ext: str): # Load valid file to check internal types - afids_set = AfidSet.load(valid_fcsv_file) + afids_set = AfidSet.load(valid_file.with_suffix(f".{ext}")) # Check correct type created after loading assert isinstance(afids_set, AfidSet) @@ -195,7 +194,7 @@ def test_invalid_fpath(self): @settings( suppress_health_check=[HealthCheck.function_scoped_fixture], ) - def test_invalid_ext(self, valid_fcsv_file: PathLike[str], ext: str): + def test_invalid_ext(self, valid_file: PathLike[str], ext: str): assume(not ext == "fcsv" or not ext == "json") with tempfile.NamedTemporaryFile( @@ -206,9 +205,9 @@ def test_invalid_ext(self, valid_fcsv_file: PathLike[str], ext: str): with pytest.raises(ValueError, match="Unsupported .* extension"): AfidSet.load(invalid_file_ext.name) - def test_invalid_label_range(self, valid_fcsv_file: PathLike[str]): + def test_invalid_label_range(self, valid_file: PathLike[str]): # Create additional line of fiducials - with open(valid_fcsv_file) as valid_fcsv: + with open(valid_file) as valid_fcsv: fcsv_data = valid_fcsv.readlines() fcsv_data.append(fcsv_data[-1]) @@ -239,7 +238,7 @@ def test_invalid_label_range(self, valid_fcsv_file: PathLike[str]): ) def test_invalid_desc( self, - valid_fcsv_file: PathLike[str], + valid_file: PathLike[str], human_mappings: list[list[str] | str], label: int, desc: str, @@ -253,7 +252,7 @@ def test_invalid_desc( ) # Replace valid description with a mismatch - with open(valid_fcsv_file) as valid_fcsv: + with open(valid_file) as valid_fcsv: fcsv_data = valid_fcsv.readlines() fcsv_data[label + 3] = fcsv_data[label + 3].replace( human_mappings[label]["desc"], desc @@ -274,46 +273,53 @@ def test_invalid_desc( ): AfidSet.load(out_fcsv_file.name) - def test_valid_save(self, valid_fcsv_file: PathLike[str]): + @given(ext=st.sampled_from(["fcsv", "json"])) + @settings( + suppress_health_check=[HealthCheck.function_scoped_fixture], + ) + def test_valid_save(self, valid_file: PathLike[str], ext: str): with tempfile.NamedTemporaryFile( - mode="w", prefix="sub-test_desc-", suffix="_afids.fcsv" + mode="w", prefix="sub-test_desc-", suffix=f"_afids.{ext}" ) as out_fcsv_file: - afids_set = AfidSet.load(valid_fcsv_file) + afids_set = AfidSet.load(valid_file.with_suffix(f".{ext}")) afids_set.save(out_fcsv_file.name) assert Path(out_fcsv_file.name).exists() @given( - ext=st.text( + ext=st.sampled_from(["fcsv", "json"]), + invalid_ext=st.text( min_size=2, max_size=5, alphabet=st.characters( min_codepoint=ord("A"), max_codepoint=ord("z") ), - ) + ), ) @settings( suppress_health_check=[HealthCheck.function_scoped_fixture], ) - def test_invalid_ext_save(self, valid_fcsv_file: PathLike[str], ext: str): - assume(not ext == "fcsv" or not ext == "json") + def test_invalid_ext_save( + self, valid_file: PathLike[str], ext: str, invalid_ext: str + ): + assume(not invalid_ext == "fcsv" or not invalid_ext == "json") with tempfile.NamedTemporaryFile( - mode="w", prefix="sub-test_desc-", suffix=f"_afids.{ext}" + mode="w", prefix="sub-test_desc-", suffix=f"_afids.{invalid_ext}" ) as out_file: - afid_set = AfidSet.load(valid_fcsv_file) + afid_set = AfidSet.load(valid_file.with_suffix(f".{ext}")) with pytest.raises(ValueError, match="Unsupported file extension"): afid_set.save(out_file.name) - @given(afid_set=af_st.afid_sets()) + @given(afid_set=af_st.afid_sets(), ext=st.sampled_from(["fcsv", "json"])) @settings( suppress_health_check=[HealthCheck.function_scoped_fixture], ) - def test_save_invalid_coord_system(self, afid_set: AfidSet): + def test_save_invalid_coord_system(self, afid_set: AfidSet, ext: str): afid_set.coord_system = "invalid" with tempfile.NamedTemporaryFile( - mode="w", prefix="sub-test_desc-", suffix="_afids.fcsv" + mode="w", prefix="sub-test_desc-", suffix=f"_afids.{ext}" ) as out_file: with pytest.raises( ValueError, match=".*invalid coordinate system" @@ -352,8 +358,8 @@ class TestAfidsCore: @settings( suppress_health_check=[HealthCheck.function_scoped_fixture], ) - def test_valid_get_afid(self, valid_fcsv_file: PathLike[str], label: int): - afid_set = AfidSet.load(valid_fcsv_file) + def test_valid_get_afid(self, valid_file: PathLike[str], label: int): + afid_set = AfidSet.load(valid_file) afid_pos = afid_set.get_afid(label) # Check array type @@ -363,10 +369,8 @@ def test_valid_get_afid(self, valid_fcsv_file: PathLike[str], label: int): @settings( suppress_health_check=[HealthCheck.function_scoped_fixture], ) - def test_invalid_get_afid( - self, valid_fcsv_file: PathLike[str], label: int - ): - afid_set = AfidSet.load(valid_fcsv_file) + def test_invalid_get_afid(self, valid_file: PathLike[str], label: int): + afid_set = AfidSet.load(valid_file) assume(not 1 <= label <= len(afid_set.afids)) with pytest.raises(InvalidFiducialError, match=".*not valid"): diff --git a/afids_utils/tests/test_ext.py b/afids_utils/tests/test_ext.py index df0d39c3..6eeed4ae 100644 --- a/afids_utils/tests/test_ext.py +++ b/afids_utils/tests/test_ext.py @@ -1,5 +1,6 @@ from __future__ import annotations +import json import re import tempfile from os import PathLike @@ -9,25 +10,33 @@ from hypothesis import HealthCheck, assume, given, settings from hypothesis import strategies as st +import afids_utils.ext.fcsv as af_fcsv +import afids_utils.ext.json as af_json from afids_utils.afids import AfidPosition, AfidSet from afids_utils.exceptions import InvalidFileError -from afids_utils.ext.fcsv import _get_afids, _get_metadata, save_fcsv from afids_utils.tests.strategies import afid_sets @pytest.fixture -def valid_fcsv_file() -> PathLike[str]: +def valid_fcsv_file() -> Path: return ( Path(__file__).parent / "data" / "tpl-MNI152NLin2009cAsym_afids.fcsv" ) +@pytest.fixture +def valid_json_file() -> Path: + return ( + Path(__file__).parent / "data" / "tpl-MNI152NLin2009cAsym_afids.json" + ) + + class TestLoadFcsv: @given(coord_num=st.integers(min_value=0, max_value=1)) @settings( suppress_health_check=[HealthCheck.function_scoped_fixture], ) - def test_get_valid_metadata( + def test_fcsv_get_valid_metadata( self, valid_fcsv_file: PathLike[str], coord_num: int ): # Randomize coordinate system @@ -47,7 +56,7 @@ def test_get_valid_metadata( temp_valid_fcsv_file.flush() with open(temp_valid_fcsv_file.name) as temp_in_fcsv: - parsed_ver, parsed_coord = _get_metadata( + parsed_ver, parsed_coord = af_fcsv._get_metadata( temp_in_fcsv.readlines() ) @@ -65,7 +74,7 @@ def test_get_valid_metadata( @settings( suppress_health_check=[HealthCheck.function_scoped_fixture], ) - def test_invalid_num_coord( + def test_fcsv_invalid_num_coord( self, valid_fcsv_file: PathLike[str], coord_num: int ): with open(valid_fcsv_file) as valid_fcsv: @@ -87,7 +96,7 @@ def test_invalid_num_coord( with pytest.raises( InvalidFileError, match="Invalid coordinate.*" ): - _get_metadata(temp_in_fcsv.readlines()) + af_fcsv._get_metadata(temp_in_fcsv.readlines()) @given( coord_str=st.text( @@ -100,7 +109,7 @@ def test_invalid_num_coord( @settings( suppress_health_check=[HealthCheck.function_scoped_fixture], ) - def test_invalid_str_coord( + def test_fcsv_invalid_str_coord( self, valid_fcsv_file: PathLike[str], coord_str: int ): assume(coord_str not in ["RAS", "LPS"]) @@ -124,9 +133,9 @@ def test_invalid_str_coord( with pytest.raises( InvalidFileError, match="Invalid coordinate.*" ): - _get_metadata(temp_in_fcsv.readlines()) + af_fcsv._get_metadata(temp_in_fcsv.readlines()) - def test_invalid_header(self, valid_fcsv_file: PathLike[str]): + def test_fcsv_invalid_header(self, valid_fcsv_file: PathLike[str]): with open(valid_fcsv_file) as valid_fcsv: valid_fcsv_data = valid_fcsv.readlines() invalid_fcsv_data = valid_fcsv_data[0] @@ -143,7 +152,7 @@ def test_invalid_header(self, valid_fcsv_file: PathLike[str]): with pytest.raises( InvalidFileError, match="Missing or invalid.*" ): - _get_metadata(temp_in_fcsv.readlines()) + af_fcsv._get_metadata(temp_in_fcsv.readlines()) @given(label=st.integers(min_value=0, max_value=31)) @settings( @@ -151,7 +160,7 @@ def test_invalid_header(self, valid_fcsv_file: PathLike[str]): ) def test_valid_get_afids(self, valid_fcsv_file: PathLike[str], label: int): with open(valid_fcsv_file) as valid_fcsv: - afids_positions = _get_afids(valid_fcsv.readlines()) + afids_positions = af_fcsv._get_afids(valid_fcsv.readlines()) assert isinstance(afids_positions, list) assert isinstance(afids_positions[label], AfidPosition) @@ -167,7 +176,7 @@ def test_save_fcsv_invalid_template( afid_set: AfidSet, ): with pytest.raises(FileNotFoundError): - save_fcsv(afid_set, "/invalid/template/path.fcsv") + af_fcsv.save_fcsv(afid_set, "/invalid/template/path.fcsv") @given(afid_set=afid_sets(randomize_header=False)) @settings( @@ -178,9 +187,154 @@ def test_save_fcsv_valid_template(self, afid_set: AfidSet): mode="w", prefix="sub-test_desc-", suffix="_afids.fcsv" ) as out_fcsv_file: # Create and check output file - save_fcsv(afid_set, out_fcsv_file.name) + af_fcsv.save_fcsv(afid_set, out_fcsv_file.name) # Check if file loads correctly and contents are the same test_load = AfidSet.load(out_fcsv_file.name) assert test_load == afid_set assert isinstance(test_load, AfidSet) + + +class TestLoadJson: + @given(coord=st.sampled_from(["RAS", "LPS", "0", "1"])) + @settings( + suppress_health_check=[HealthCheck.function_scoped_fixture], + ) + def test_json_get_valid_metadata( + self, valid_json_file: PathLike[str], coord: str + ): + # Randomize coordinate system + with open(valid_json_file) as valid_json: + afids_json = json.load(valid_json) + afids_json["markups"][0]["coordinateSystem"] = coord + + with tempfile.NamedTemporaryFile( + mode="w", + prefix="sub-test_desc-", + suffix="_afids.json", + ) as temp_valid_json_file: + json.dump(afids_json, temp_valid_json_file, indent=4) + temp_valid_json_file.flush() + + with open(temp_valid_json_file.name) as temp_in_json: + temp_afids_json = json.load(temp_in_json) + parsed_ver, parsed_coord = af_json._get_metadata( + temp_afids_json["markups"][0]["coordinateSystem"] + ) + + # Check version is not given / unknown + assert parsed_ver == "Unknown" + + # Check to make sure coordinate system is correct + if coord in ["0" or "RAS"]: + assert parsed_coord == "RAS" + elif coord in ["1" or "LPS"]: + assert parsed_coord == "LPS" + + @given(coord_num=st.integers(min_value=2)) + @settings( + suppress_health_check=[HealthCheck.function_scoped_fixture], + ) + def test_json_invalid_num_coord( + self, valid_json_file: PathLike[str], coord_num: int + ): + with open(valid_json_file) as valid_json: + afids_json = json.load(valid_json) + afids_json["markups"][0]["coordinateSystem"] = str(coord_num) + + with tempfile.NamedTemporaryFile( + mode="w", + prefix="sub-test_desc-", + suffix="_afids.json", + ) as temp_valid_json_file: + json.dump(afids_json, temp_valid_json_file, indent=4) + temp_valid_json_file.flush() + + with open(temp_valid_json_file.name) as temp_in_json: + with pytest.raises( + InvalidFileError, match=r"Invalid coordinate.*" + ): + temp_afids_json = json.load(temp_in_json) + af_json._get_metadata(temp_afids_json["markups"][0]) + + @given( + coord_str=st.text( + min_size=3, + alphabet=st.characters( + min_codepoint=ord("A"), max_codepoint=ord("z") + ), + ) + ) + @settings( + suppress_health_check=[HealthCheck.function_scoped_fixture], + ) + def test_json_invalid_str_coord( + self, valid_json_file: PathLike[str], coord_str: str + ): + assume(coord_str not in ["RAS", "LPS"]) + + with open(valid_json_file) as valid_json: + afids_json = json.load(valid_json) + afids_json["markups"][0]["coordinateSystem"] = coord_str + + with tempfile.NamedTemporaryFile( + mode="w", + prefix="sub-test_desc-", + suffix="_afids.json", + ) as temp_valid_json_file: + json.dump(afids_json, temp_valid_json_file, indent=4) + temp_valid_json_file.flush() + + with open(temp_valid_json_file.name) as temp_in_json: + with pytest.raises( + InvalidFileError, match=r"Invalid coordinate.*" + ): + temp_afids_json = json.load(temp_in_json) + af_json._get_metadata(temp_afids_json["markups"][0]) + + @given(label=st.integers(min_value=0, max_value=31)) + @settings( + suppress_health_check=[HealthCheck.function_scoped_fixture], + ) + def test_json_valid_get_afids( + self, valid_json_file: PathLike[str], label: int + ): + with open(valid_json_file) as valid_json: + afids_json = json.load(valid_json) + afids_positions = af_json._get_afids( + afids_json["markups"][0]["controlPoints"] + ) + + assert isinstance(afids_positions, list) + assert isinstance(afids_positions[label], AfidPosition) + + +class TestSaveJson: + @given(afid_set=afid_sets()) + @settings( + suppress_health_check=[HealthCheck.function_scoped_fixture], + ) + def test_save_json_invalid_template( + self, + afid_set: AfidSet, + ): + with pytest.raises(FileNotFoundError): + af_json.save_json(afid_set, "/invalid/template/path.json") + + @given(afid_set=afid_sets(randomize_header=False)) + @settings( + suppress_health_check=[HealthCheck.function_scoped_fixture], + ) + def test_save_json_valid_template(self, afid_set: AfidSet): + with tempfile.NamedTemporaryFile( + mode="w", prefix="sub-test_desc-", suffix="_afids.json" + ) as out_json_file: + # Create and check output file + af_json.save_json(afid_set, out_json_file.name) + + # Check if file loads correctly and contents are the same + # (except for the version) + test_load = AfidSet.load(out_json_file.name) + assert test_load.coord_system == afid_set.coord_system + assert test_load.afids == afid_set.afids + assert isinstance(test_load, AfidSet)