From 55c7a44dca2032d571e8425b602a813d0413ad70 Mon Sep 17 00:00:00 2001 From: Xiao Gui Date: Fri, 4 Oct 2024 15:15:55 +0200 Subject: [PATCH] revert: use old worker for all but /assign endpoints --- VERSION | 2 +- api/common/data_handlers/core/__init__.py | 1 + api/common/data_handlers/core/misc.py | 216 ++++++++++++++++++ api/common/data_handlers/features/__init__.py | 2 +- api/common/data_handlers/features/misc.py | 24 ++ api/server/volcabularies/__init__.py | 3 +- api/server/volumes/parcellationmap.py | 28 ++- api/siibra_api_config.py | 2 +- 8 files changed, 267 insertions(+), 11 deletions(-) create mode 100644 api/common/data_handlers/core/misc.py create mode 100644 api/common/data_handlers/features/misc.py diff --git a/VERSION b/VERSION index 719cd124..771f209c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.19 \ No newline at end of file +0.3.20 \ No newline at end of file diff --git a/api/common/data_handlers/core/__init__.py b/api/common/data_handlers/core/__init__.py index 99810fb8..740883c8 100644 --- a/api/common/data_handlers/core/__init__.py +++ b/api/common/data_handlers/core/__init__.py @@ -2,3 +2,4 @@ from . import space from . import region from . import parcellation +from . import misc \ No newline at end of file diff --git a/api/common/data_handlers/core/misc.py b/api/common/data_handlers/core/misc.py new file mode 100644 index 00000000..437288d6 --- /dev/null +++ b/api/common/data_handlers/core/misc.py @@ -0,0 +1,216 @@ +from api.common import data_decorator, get_filename, NotFound +from api.models.volumes.volume import MapType +from api.siibra_api_config import ROLE +from typing import Union, Dict, Tuple + +@data_decorator(ROLE) +def get_map(parcellation_id: str, space_id: str, maptype: Union[MapType, str]) -> Dict: + """Get a map instance, based on specification + + Args: + parcellation_id: lookup id of the parcellation of the map + space_id: lookup id of the space of the map + maptype: maptype, either LABELLED or STATISTICAL + + Returns: + Requested map instance, serialized into dict + + Raises: + AssertionError: if the supplied maptype is invalid type + NotFound: Map with the specification not found + """ + import siibra + from api.serialization.util import instance_to_model + + maptype_string = None + # check maptype name and value both matches + if isinstance(maptype, MapType): + assert maptype.name == maptype.value, f"str enum, expecting .name and .value to equal" + maptype_string = maptype.name + if isinstance(maptype, str): + maptype_string = maptype + + assert maptype_string is not None, f"maptype is neither MapType nor str" + + siibra_maptype = siibra.MapType[maptype_string] + assert siibra_maptype.name == maptype_string, f"Expecting maptype.name to match" + + returned_map = siibra.get_map(parcellation_id, space_id, siibra_maptype) + + if returned_map is None: + raise NotFound + + return instance_to_model( + returned_map + ).dict() + + +def cache_region_statistic_map(parcellation_id: str, region_id: str, space_id: str) -> Tuple[str, bool]: + """Retrieve and save regional statistical map (if necessary), and then return the path of the map. + + Args: + parcellation_id: lookup id of the parcellation of the map + region_id: lookup id of the region of the map + space_id: lookup id of the space of the map + + Returns: + path to statistical map, if a cached file is returned + """ + import os + full_filename = get_filename("statistical_map", parcellation_id, region_id, space_id, ext=".nii.gz") + if os.path.isfile(full_filename): + return full_filename, True + + import siibra + import nibabel as nib + error_text = f"Map with parc id '{parcellation_id}', space id '{space_id}'" + + stat_map = siibra.get_map(parcellation_id, space_id, siibra.MapType.STATISTICAL) + assert stat_map is not None, f"{error_text} returns None" + + volume_data = stat_map.fetch(region=region_id) + + error_text = f"{error_text}, with region_id '{region_id}'" + assert isinstance(volume_data, nib.Nifti1Image), f"{error_text}, volume provided is not of type Nifti1Image" + + nib.save(volume_data, full_filename) + import json + import time + with open(f"{full_filename}.{str(int(time.time()))}.json", "w") as fp: + json.dump({ + "prefix": "statistical_map", + "parcellation_id": parcellation_id, + "region_id": region_id, + "space_id": space_id, + }, fp=fp, indent="\t") + return full_filename, False + +@data_decorator(ROLE) +def get_region_statistic_map(parcellation_id: str, region_id: str, space_id: str): + """Retrieve and save regional statistical map (if necessary), and then return the path of the map. + + Args: + parcellation_id: lookup id of the parcellation of the map + region_id: lookup id of the region of the map + space_id: lookup id of the space of the map + + Returns: + path to statistical map, if a cached file is returned + """ + return cache_region_statistic_map(parcellation_id, region_id, space_id) + +@data_decorator(ROLE) +def get_region_statistic_map_info(parcellation_id: str, region_id: str, space_id: str): + """Retrieve and save regional statistical map (if necessary), and then return the path of the map. + + Args: + parcellation_id: lookup id of the parcellation of the map + region_id: lookup id of the region of the map + space_id: lookup id of the space of the map + + Returns: + dict of min an max of the statistical map + """ + full_filename, _cache_flag = cache_region_statistic_map(parcellation_id, region_id, space_id) + + import nibabel as nib + import numpy as np + + nii = nib.load(full_filename) + data = nii.get_fdata() + return { + "min": np.min(data), + "max": np.max(data), + } + +@data_decorator(ROLE) +def get_parcellation_labelled_map(parcellation_id: str, space_id: str, region_id:str=None): + """Retrieve and save labelled map / regional mask (if necessary), and then return the path of the map. + + Args: + parcellation_id: lookup id of the parcellation of the map + region_id: lookup id of the region of the map + space_id: lookup id of the space of the map + + Returns: + path to labelled map/regional mask, if a cached file is returned + """ + import os + full_filename = get_filename("labelled_map", parcellation_id, space_id, region_id if region_id else "", ext=".nii.gz") + if os.path.isfile(full_filename): + return full_filename, True + + import siibra + import nibabel as nib + error_text = f"Map with parc id '{parcellation_id}', space id '{space_id}'" + + volume_data = None + if region_id is not None: + region = siibra.get_region(parcellation_id, region_id) + volume_data = region.fetch_regional_map(space_id, siibra.MapType.LABELLED) + else: + labelled_map = siibra.get_map(parcellation_id, space_id, siibra.MapType.LABELLED) + assert labelled_map is not None, f"{error_text} returns None" + volume_data = labelled_map.fetch() + + assert isinstance(volume_data, nib.Nifti1Image), f"{error_text}, volume provided is not of type Nifti1Image" + + nib.save(volume_data, full_filename) + import json + import time + with open(f"{full_filename}.{str(int(time.time()))}.json", "w") as fp: + json.dump({ + "prefix": "labelled_map", + "parcellation_id": parcellation_id, + "space_id": space_id, + "region_id": region_id, + }, fp=fp, indent="\t") + return full_filename, False + +# @data_decorator(ROLE) +# def assign_point(parcellation_id: str, space_id: str, point: str, assignment_type: str, sigma_mm: float): +# import siibra +# from api.serialization.util import instance_to_model +# m = siibra.get_map(parcellation_id, space_id, assignment_type) +# p = siibra.Point(point, space=space_id, sigma_mm=sigma_mm) +# df = m.assign(p) + +# try: +# return instance_to_model(df, detail=True).dict() +# except Exception as e: +# raise e + +@data_decorator(ROLE) +def get_resampled_map(parcellation_id: str, space_id: str): + """Retrieve and save a labelled map, resampled in space (if necessary), and then return the path of the map. + + Args: + parcellation_id: lookup id of the parcellation of the map + space_id: lookup id of the target space of the sampled map + + Returns: + path to statistical map, if a cached file is returned + """ + import os + full_filename = get_filename("resampled_map", parcellation_id, space_id, ext=".nii.gz") + if os.path.isfile(full_filename): + return full_filename, True + + import siibra + import nibabel as nib + parcellation: siibra.core.parcellation.Parcellation = siibra.parcellations[parcellation_id] + parcellation_map = parcellation.get_map(siibra.spaces[space_id], siibra.MapType.LABELLED) + nii = parcellation_map.get_resampled_template() + + assert isinstance(nii, nib.Nifti1Image), f"resample failed... returned not of type nii" + + import time + import json + nib.save(nii, full_filename) + with open(f"{full_filename}.{str(int(time.time()))}.json", "w") as fp: + json.dump({ + "prefix": "resampled_map", + "parcellation_id": parcellation_id, + "space_id": space_id, + }, indent="\t", fp=fp) + return full_filename, False \ No newline at end of file diff --git a/api/common/data_handlers/features/__init__.py b/api/common/data_handlers/features/__init__.py index 58252c38..3b8280c7 100644 --- a/api/common/data_handlers/features/__init__.py +++ b/api/common/data_handlers/features/__init__.py @@ -1 +1 @@ -from . import types +from . import types, misc \ No newline at end of file diff --git a/api/common/data_handlers/features/misc.py b/api/common/data_handlers/features/misc.py new file mode 100644 index 00000000..8263fdb9 --- /dev/null +++ b/api/common/data_handlers/features/misc.py @@ -0,0 +1,24 @@ +from api.common import data_decorator +from api.siibra_api_config import ROLE +from api.models.vocabularies.genes import GeneModel + +@data_decorator(ROLE) +def get_genes(find:str=None): + """Get all genes + + Args: + string to find in vocabularies + + Returns: + List of the genes.""" + + from siibra.vocabularies import GENE_NAMES + if find == None: + return_list = [v for v in GENE_NAMES] + else: + return_list = GENE_NAMES.find(find) + + return [GeneModel( + symbol=v.get("symbol"), + description=v.get("description") + ).dict() for v in return_list] diff --git a/api/server/volcabularies/__init__.py b/api/server/volcabularies/__init__.py index 2f82cc1e..3d4568c8 100644 --- a/api/server/volcabularies/__init__.py +++ b/api/server/volcabularies/__init__.py @@ -7,7 +7,8 @@ from api.siibra_api_config import ROLE from api.common import router_decorator from api.models.vocabularies.genes import GeneModel -from api.common.data_handlers.vocabularies.gene import get_genes +# from api.common.data_handlers.vocabularies.gene import get_genes +from api.common.data_handlers.features.misc import get_genes TAGS= ["vocabularies"] """HTTP vocabularies tags""" diff --git a/api/server/volumes/parcellationmap.py b/api/server/volumes/parcellationmap.py index 2a7e05c2..0da6fb9c 100644 --- a/api/server/volumes/parcellationmap.py +++ b/api/server/volumes/parcellationmap.py @@ -10,6 +10,13 @@ from api.models.volumes.volume import MapType from api.models._commons import DataFrameModel from api.common import router_decorator, get_filename, logger, NotFound +from api.common.data_handlers.core.misc import ( + get_map as old_get_map, + get_resampled_map as old_get_resampled_map, + get_parcellation_labelled_map as old_get_parcellation_labelled_map, + get_region_statistic_map as old_get_region_statistic_map, + get_region_statistic_map_info as old_get_region_statistic_map_info +) from new_api.v3.data_handlers.map import assign, get_map, statistical_map_info_json, statistical_map_nii_gz, labelled_map_nii_gz, resampled_template from api.server.util import SapiCustomRoute import os @@ -20,20 +27,23 @@ router = APIRouter(route_class=SapiCustomRoute, tags=TAGS) """HTTP map router""" +# still use the old worker. New worker not stable (?) @router.get("", response_model=MapModel) @version(*FASTAPI_VERSION) -@router_decorator(ROLE, func=get_map) +@router_decorator(ROLE, func=old_get_map) def get_siibra_map(parcellation_id: str, space_id: str, map_type: MapType, name: str= "", *, func): """Get map according to specification""" if func is None: raise HTTPException(500, f"func: None passsed") return func(parcellation_id, space_id, map_type, name) + +# still use the old worker. New worker not stable (?) @router.get("/resampled_template", response_class=FileResponse, tags=TAGS, description=""" Return a resampled template volume, based on labelled parcellation map. """) @version(*FASTAPI_VERSION) -@router_decorator(ROLE, func=resampled_template) +@router_decorator(ROLE, func=old_get_resampled_map) def get_resampled_map(parcellation_id: str, space_id: str, *, func): """Get resampled map according to specification""" if func is None: @@ -50,6 +60,7 @@ def get_resampled_map(parcellation_id: str, space_id: str, *, func): return FileResponse(full_filename, headers=headers) +# still use the old worker. New worker not stable (?) @router.get("/labelled_map.nii.gz", response_class=FileResponse, tags=TAGS, description=""" Returns a labelled map if region_id is not provided. @@ -58,7 +69,7 @@ def get_resampled_map(parcellation_id: str, space_id: str, *, func): region_id MAY refer to ANY region on the region hierarchy, and a combined mask will be returned. """) @version(*FASTAPI_VERSION) -@router_decorator(ROLE, func=labelled_map_nii_gz) +@router_decorator(ROLE, func=old_get_parcellation_labelled_map) def get_parcellation_labelled_map(parcellation_id: str, space_id: str, region_id: str=None, *, func): """Get labelled map according to specification""" if func is None: @@ -76,13 +87,14 @@ def get_parcellation_labelled_map(parcellation_id: str, space_id: str, region_id return FileResponse(full_filename, headers=headers) +# still use the old worker. New worker not stable (?) @router.get("/statistical_map.nii.gz", response_class=FileResponse, tags=TAGS, description=""" Returns a statistic map. region_id MUST refer to leaf region on the region hierarchy. """) @version(*FASTAPI_VERSION) -@router_decorator(ROLE, func=statistical_map_nii_gz) +@router_decorator(ROLE, func=old_get_region_statistic_map) def get_region_statistical_map(parcellation_id: str, region_id: str, space_id: str, name: str="", *, func): """Get statistical map according to specification""" if func is None: @@ -92,7 +104,7 @@ def get_region_statistical_map(parcellation_id: str, region_id: str, space_id: s "content-type": "application/octet-stream", "content-disposition": f'attachment; filename="statistical_map.nii.gz"' } - full_filename, cache_flag = func(parcellation_id=parcellation_id, region_id=region_id, space_id=space_id, name=name) + full_filename, cache_flag = func(parcellation_id=parcellation_id, region_id=region_id, space_id=space_id) if cache_flag: headers[cache_header] = "hit" assert os.path.isfile(full_filename), f"file saved incorrectly" @@ -102,15 +114,17 @@ class StatisticModelInfo(BaseModel): min: float max: float + +# still use the old worker. New worker not stable (?) @router.get("/statistical_map.info.json", response_model=StatisticModelInfo, tags=TAGS) @version(*FASTAPI_VERSION) -@router_decorator(ROLE, func=statistical_map_info_json) +@router_decorator(ROLE, func=old_get_region_statistic_map_info) def get_region_statistical_map_metadata(parcellation_id: str, region_id: str, space_id: str, name: str="", *, func): """Get metadata of statistical map according to specification""" if func is None: raise HTTPException(500, f"func: None passsed") - data = func(parcellation_id=parcellation_id, region_id=region_id, space_id=space_id, name=name) + data = func(parcellation_id=parcellation_id, region_id=region_id, space_id=space_id) return StatisticModelInfo(**data) @router.get("/assign", response_model=DataFrameModel, tags=TAGS) diff --git a/api/siibra_api_config.py b/api/siibra_api_config.py index 53521441..d5cf6525 100644 --- a/api/siibra_api_config.py +++ b/api/siibra_api_config.py @@ -70,7 +70,7 @@ def get_config_dir_short_hash(path_to_config: str): "features", "volumes", "compounds", - "vocabularies", + # "vocabularies", ] class CELERY_CONFIG: