diff --git a/src/diffcalc_api/__init__.py b/src/diffcalc_api/__init__.py index c8ba73b..0d842ac 100644 --- a/src/diffcalc_api/__init__.py +++ b/src/diffcalc_api/__init__.py @@ -1,6 +1,6 @@ """API to expose diffcalc-core methods.""" -from . import config, database, openapi, server +from . import config, database, openapi, server, types from ._version_git import __version__ -__all__ = ["__version__", "server", "config", "database", "openapi"] +__all__ = ["__version__", "server", "config", "database", "types", "openapi"] diff --git a/src/diffcalc_api/models/response.py b/src/diffcalc_api/models/response.py index 1ae7f3a..f927bb8 100644 --- a/src/diffcalc_api/models/response.py +++ b/src/diffcalc_api/models/response.py @@ -4,6 +4,7 @@ from pydantic import BaseModel from diffcalc_api.models.ub import HklModel, MiscutModel, SphericalCoordinates, XyzModel +from diffcalc_api.types import Orientation, Reflection class InfoResponse(BaseModel): @@ -80,3 +81,31 @@ class MiscutResponse(BaseModel): """ payload: MiscutModel + + +class ReflectionResponse(BaseModel): + """Response for any operation returning a reflection.""" + + payload: Reflection + + class Config: + """Necessary config to make validation easier. + + As this is a response model, there is no need to enforce validation. + """ + + orm_mode = True + + +class OrientationResponse(BaseModel): + """Response for any operation returning an orientation.""" + + payload: Orientation + + class Config: + """Necessary config to make validation easier. + + As this is a response model, there is no need to enforce validation. + """ + + orm_mode = True diff --git a/src/diffcalc_api/routes/ub.py b/src/diffcalc_api/routes/ub.py index 96bd73e..4d80295 100644 --- a/src/diffcalc_api/routes/ub.py +++ b/src/diffcalc_api/routes/ub.py @@ -14,7 +14,9 @@ ArrayResponse, InfoResponse, MiscutResponse, + OrientationResponse, ReciprocalSpaceResponse, + ReflectionResponse, SphericalResponse, StringResponse, ) @@ -33,6 +35,7 @@ ) from diffcalc_api.services import ub as service from diffcalc_api.stores.protocol import HklCalcStore, get_store +from diffcalc_api.types import Orientation, Position, Reflection router = APIRouter(prefix="/ub", tags=["ub"]) @@ -62,6 +65,48 @@ async def get_ub_status( ####################################################################################### +@router.get("/{name}/reflection", response_model=ReflectionResponse) +async def get_reflection( + name: str, + store: HklCalcStore = Depends(get_store), + collection: Optional[str] = Query(default=None, example="B07"), + tag: Optional[str] = Query(default=None, example="refl1"), + idx: Optional[int] = Query(default=None), +): + """Get a reflection from the UBCalculation object. + + Both tag and idx cannot be provided. One or the other must be provided. + + Args: + name: the name of the hkl object to access within the store + store: accessor to the hkl object + collection: collection within which the hkl object resides + tag: optional tag to access the reflection + idx: optional index to access the reflection + + Returns: + ReflectionResponse + payload containing the reflection. + """ + if (tag is None) and (idx is None): + raise NoTagOrIdxProvidedError() + + if (tag is not None) and (idx is not None): + raise BothTagAndIdxProvidedError() + + ref = await service.get_reflection(name, store, collection, tag, idx) + + reflection = Reflection( + ref.h, + ref.k, + ref.l, + Position(**ref.pos.asdict), + ref.energy, + ref.tag, + ) + return ReflectionResponse(payload=reflection) + + @router.post("/{name}/reflection", response_model=InfoResponse) async def add_reflection( name: str, @@ -163,6 +208,50 @@ async def delete_reflection( ####################################################################################### +@router.get("/{name}/orientation", response_model=OrientationResponse) +async def get_orientation( + name: str, + store: HklCalcStore = Depends(get_store), + collection: Optional[str] = Query(default=None, example="B07"), + tag: Optional[str] = Query(default=None, example="refl1"), + idx: Optional[int] = Query(default=None), +): + """Get an orientation from the UBCalculation object. + + Both tag and idx cannot be provided. One or the other must be provided. + + Args: + name: the name of the hkl object to access within the store + store: accessor to the hkl object + collection: collection within which the hkl object resides + tag: optional tag to access the orientation + idx: optional index to access the orientation + + Returns: + OrientationResponse + payload containing the orientation. + """ + if (tag is None) and (idx is None): + raise NoTagOrIdxProvidedError() + + if (tag is not None) and (idx is not None): + raise BothTagAndIdxProvidedError() + + orient = await service.get_orientation(name, store, collection, tag, idx) + + orientation = Orientation( + orient.h, + orient.k, + orient.l, + orient.x, + orient.y, + orient.z, + Position(**orient.pos.asdict), + orient.tag if orient.tag is not None else "", + ) + return OrientationResponse(payload=orientation) + + @router.post("/{name}/orientation", response_model=InfoResponse) async def add_orientation( name: str, diff --git a/src/diffcalc_api/services/ub.py b/src/diffcalc_api/services/ub.py index 7f898cf..fdfd593 100644 --- a/src/diffcalc_api/services/ub.py +++ b/src/diffcalc_api/services/ub.py @@ -5,6 +5,7 @@ import numpy as np from diffcalc.hkl.geometry import Position from diffcalc.ub.calc import UBCalculation +from diffcalc.ub.reference import Orientation, Reflection from diffcalc_api.errors.ub import ( InvalidIndexError, @@ -48,6 +49,45 @@ async def get_ub_status( ####################################################################################### +async def get_reflection( + name: str, + store: HklCalcStore, + collection: Optional[str], + tag: Optional[str], + idx: Optional[int], +) -> Reflection: + """Get a reflection from the UBCalculation object. + + Both tag and idx cannot be provided. One or the other must be provided. + This is enforced in the route for this function, see diffcalc_api/routes/ub.py. + An error is thrown if the reflection does not exist, i.e. cannot be retrieved. + + Args: + name: the name of the hkl object to access within the store + store: accessor to the hkl object + collection: collection within which the hkl object resides + tag: optional tag to access the reflection + idx: optional index to access the reflection + + Returns: + Reflection + A reflection object, as defined in diffcalc_api.types. + """ + hklcalc = await store.load(name, collection) + ubcalc: UBCalculation = hklcalc.ubcalc + + retrieve: Union[int, str] = ( + tag if tag is not None else (idx if idx is not None else 0) + ) + + try: + reflection = ubcalc.get_reflection(retrieve) + except (IndexError, ValueError): + raise ReferenceRetrievalError(retrieve, "reflection") + + return reflection + + async def add_reflection( name: str, params: AddReflectionParams, @@ -167,6 +207,45 @@ async def delete_reflection( ####################################################################################### +async def get_orientation( + name: str, + store: HklCalcStore, + collection: Optional[str], + tag: Optional[str], + idx: Optional[int], +) -> Orientation: + """Get an orientation from the UBCalculation object. + + Both tag and idx cannot be provided. One or the other must be provided. + This is enforced in the route for this function, see diffcalc_api/routes/ub.py. + An error is thrown if the orientation does not exist, i.e. cannot be retrieved. + + Args: + name: the name of the hkl object to access within the store + store: accessor to the hkl object + collection: collection within which the hkl object resides + tag: optional tag to access the orientation + idx: optional index to access the orientation + + Returns: + Orientation + An orientation object, as defined in diffcalc_api.types. + """ + hklcalc = await store.load(name, collection) + ubcalc: UBCalculation = hklcalc.ubcalc + + retrieve: Union[int, str] = ( + tag if tag is not None else (idx if idx is not None else 0) + ) + + try: + orientation = ubcalc.get_orientation(retrieve) + except (IndexError, ValueError): + raise ReferenceRetrievalError(retrieve, "orientation") + + return orientation + + async def add_orientation( name: str, params: AddOrientationParams, @@ -226,7 +305,7 @@ async def edit_orientation( try: orientation = hklcalc.ubcalc.get_orientation(retrieve) except (IndexError, ValueError): - raise ReferenceRetrievalError(retrieve, "reflection") + raise ReferenceRetrievalError(retrieve, "orientation") inputs = { "idx": retrieve, diff --git a/src/diffcalc_api/types.py b/src/diffcalc_api/types.py new file mode 100644 index 0000000..e01f190 --- /dev/null +++ b/src/diffcalc_api/types.py @@ -0,0 +1,51 @@ +"""Exposes types of diffcalc-core objects.""" + +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class Position: + """Diffractometer angles in degrees. + + Similar to diffcalc.hkl.geometry.Position + """ + + mu: float + delta: float + nu: float + eta: float + chi: float + phi: float + + +@dataclass +class Orientation: + """Reference orientation of the sample. + + Similar to diffcalc.ub.reference.Orientation + """ + + h: float + k: float + l: float + x: float + y: float + z: float + pos: Position + tag: Optional[str] + + +@dataclass +class Reflection: + """Reference reflection of the sample. + + Similar to diffcalc.ub.reference.Reflection + """ + + h: float + k: float + l: float + pos: Position + energy: float + tag: Optional[str] diff --git a/tests/test_ubcalc.py b/tests/test_ubcalc.py index 2aa0445..897e699 100644 --- a/tests/test_ubcalc.py +++ b/tests/test_ubcalc.py @@ -8,13 +8,17 @@ from diffcalc.hkl.constraints import Constraints from diffcalc.hkl.geometry import Position from diffcalc.ub.calc import UBCalculation +from diffcalc.ub.reference import Orientation, Reflection from fastapi.testclient import TestClient from diffcalc_api.errors.ub import ( + BothTagAndIdxProvidedError, ErrorCodes, InvalidIndexError, NoCrystalError, + NoTagOrIdxProvidedError, NoUbMatrixError, + ReferenceRetrievalError, ) from diffcalc_api.server import app from diffcalc_api.stores.protocol import get_store @@ -114,6 +118,42 @@ def test_add_reflection(): assert ubcalc.get_reflection("foo") +def test_get_reflection(): + ubcalc = UBCalculation() + hkl = HklCalculation(ubcalc, Constraints()) + client = Client(hkl).client + + ref = Reflection(0.0, 1.0, 0.0, Position(7, 0, 10, 0, 0, 0), 12, "") + ubcalc.reflist.reflections.append(ref) + + response = client.get("/ub/test/reflection?idx=1") + + assert response.status_code == 200 + assert literal_eval(response.content.decode())["payload"] == ref.asdict + + +def test_get_reflection_fails_for_wrong_inputs(): + ubcalc = UBCalculation() + hkl = HklCalculation(ubcalc, Constraints()) + client = Client(hkl).client + + ref = Reflection(0.0, 1.0, 0.0, Position(7, 0, 10, 0, 0, 0), 12, "") + ubcalc.reflist.reflections.append(ref) + + response_no_idx_or_tag = client.get("/ub/test/reflection?") + response_both_idx_and_tag = client.get("/ub/test/reflection?idx=1&tag=two") + response_wrong_idx = client.get("/ub/test/reflection?idx=2") + response_wrong_tag = client.get("/ub/test/reflection?tag=two") + + def select_type(response): + return literal_eval(response.content.decode())["type"] + + assert select_type(response_no_idx_or_tag) == str(NoTagOrIdxProvidedError) + assert select_type(response_both_idx_and_tag) == str(BothTagAndIdxProvidedError) + assert select_type(response_wrong_idx) == str(ReferenceRetrievalError) + assert select_type(response_wrong_tag) == str(ReferenceRetrievalError) + + def test_edit_reflection(): ubcalc = UBCalculation() hkl = HklCalculation(ubcalc, Constraints()) @@ -186,6 +226,42 @@ def test_add_orientation(): assert ubcalc.get_orientation("bar") +def test_get_orientation(): + ubcalc = UBCalculation() + hkl = HklCalculation(ubcalc, Constraints()) + client = Client(hkl).client + + orient = Orientation(0.0, 1.0, 0.0, 1.0, 0.0, 0.0, Position(), "") + ubcalc.orientlist.orientations.append(orient) + + response = client.get("/ub/test/orientation?idx=1") + + assert response.status_code == 200 + assert literal_eval(response.content.decode())["payload"] == orient.asdict + + +def test_get_orientation_fails_for_wrong_inputs(): + ubcalc = UBCalculation() + hkl = HklCalculation(ubcalc, Constraints()) + client = Client(hkl).client + + orient = Orientation(0.0, 1.0, 0.0, 0.0, 1.0, 0.0, Position(7, 0, 10, 0, 0, 0), "") + ubcalc.orientlist.orientations.append(orient) + + response_no_idx_or_tag = client.get("/ub/test/orientation?") + response_both_idx_and_tag = client.get("/ub/test/orientation?idx=1&tag=two") + response_wrong_idx = client.get("/ub/test/orientation?idx=2") + response_wrong_tag = client.get("/ub/test/orientation?tag=two") + + def select_type(response): + return literal_eval(response.content.decode())["type"] + + assert select_type(response_no_idx_or_tag) == str(NoTagOrIdxProvidedError) + assert select_type(response_both_idx_and_tag) == str(BothTagAndIdxProvidedError) + assert select_type(response_wrong_idx) == str(ReferenceRetrievalError) + assert select_type(response_wrong_tag) == str(ReferenceRetrievalError) + + def test_edit_orientation(): ubcalc = UBCalculation() hkl = HklCalculation(ubcalc, Constraints())