diff --git a/.vscode/launch.json b/.vscode/launch.json index f8fcdb4..2e54e7f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,6 +4,18 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + { + "name": "Python: FastAPI", + "type": "python", + "request": "launch", + "module": "uvicorn", + "args": [ + "diffcalc_api.server:app", + "--reload" + ], + "jinja": true, + "justMyCode": true + }, { "name": "Debug Unit Test", "type": "python", diff --git a/src/diffcalc_api/errors/ub.py b/src/diffcalc_api/errors/ub.py index 1cb0515..d0d6fcf 100644 --- a/src/diffcalc_api/errors/ub.py +++ b/src/diffcalc_api/errors/ub.py @@ -20,6 +20,7 @@ class ErrorCodes(ErrorCodesBase): INVALID_PROPERTY = 400 NO_TAG_OR_IDX_PROVIDED = 400 BOTH_TAG_OR_IDX_PROVIDED = 400 + NO_UB_MATRIX_ERROR = 400 responses = {code: ALL_RESPONSES[code] for code in np.unique(ErrorCodes.all_codes())} @@ -91,3 +92,16 @@ def __init__(self): """Set detail and status code.""" self.detail = f"invalid property. Choose one of: {VECTOR_PROPERTIES}" self.status_code = ErrorCodes.INVALID_PROPERTY + + +class NoUbMatrixError(DiffcalcAPIException): + """When there is no U/UB matrix, some commands in diffcalc-core fail.""" + + def __init__(self): + """Set detail and status code.""" + self.detail = ( + "It seems like there is no UB matrix for this record. Please " + + "try again after setting the UB matrix, either by calculating the UB from" + + " existing reflections/orientations or setting it explicitly." + ) + self.status_code = ErrorCodes.NO_UB_MATRIX_ERROR diff --git a/src/diffcalc_api/models/response.py b/src/diffcalc_api/models/response.py index dfdff4b..d3b4ae7 100644 --- a/src/diffcalc_api/models/response.py +++ b/src/diffcalc_api/models/response.py @@ -3,7 +3,7 @@ from pydantic import BaseModel -from diffcalc_api.models.ub import HklModel +from diffcalc_api.models.ub import HklModel, MiscutModel class InfoResponse(BaseModel): @@ -54,3 +54,12 @@ class MillerIndicesResponse(BaseModel): """ payload: HklModel + + +class MiscutResponse(BaseModel): + """Response for any operation returning miscuts. + + Only used for endpoints in ub routes. + """ + + payload: MiscutModel diff --git a/src/diffcalc_api/models/ub.py b/src/diffcalc_api/models/ub.py index 1f349f9..524c917 100644 --- a/src/diffcalc_api/models/ub.py +++ b/src/diffcalc_api/models/ub.py @@ -85,3 +85,13 @@ def select_idx_or_tag_str(idx: Optional[int], tag: Optional[str]) -> str: Return a string for diffcalc_api.models.response.InfoResponse endpoint responses. """ return f"index {idx}" if idx is not None else f"tag {tag}" + + +class MiscutModel(BaseModel): + """Describe a miscut. + + Miscut requires a rotation axis definition, and an angle from this rotation axis. + """ + + angle: float + rotation_axis: XyzModel diff --git a/src/diffcalc_api/routes/hkl.py b/src/diffcalc_api/routes/hkl.py index e098a26..c44fb6f 100644 --- a/src/diffcalc_api/routes/hkl.py +++ b/src/diffcalc_api/routes/hkl.py @@ -7,7 +7,6 @@ from diffcalc_api.errors.hkl import InvalidSolutionBoundsError from diffcalc_api.models.hkl import SolutionConstraints from diffcalc_api.models.response import ( - ArrayResponse, DiffractorAnglesResponse, MillerIndicesResponse, ScanResponse, @@ -19,42 +18,6 @@ router = APIRouter(prefix="/hkl", tags=["hkl"]) -@router.get("/{name}/UB", response_model=ArrayResponse) -async def calculate_ub( - name: str, - tag1: Optional[str] = Query(default=None, example="refl1"), - idx1: Optional[int] = Query(default=None), - tag2: Optional[str] = Query(default=None, example="plane"), - idx2: Optional[int] = Query(default=None), - store: HklCalcStore = Depends(get_store), - collection: Optional[str] = Query(default=None, example="B07"), -): - """Calculate the UB matrix. - - 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. - tag1: the tag of the first reference object. - idx1: the index of the first reference object. - tag2: the tag of the second reference object. - idx2: the index of the second reference object. - - For each reference object, only a tag or index needs to be given. If none are - provided, diffcalc-core tries to work it out from the available reference - objects. - - Returns: - ArrayResponse object containing a list of angles, combined together into one - dictionary. - - """ - content = await service.calculate_ub( - name, store, collection, tag1, idx1, tag2, idx2 - ) - return ArrayResponse(payload=content) - - @router.get("/{name}/position/lab", response_model=DiffractorAnglesResponse) async def lab_position_from_miller_indices( name: str, diff --git a/src/diffcalc_api/routes/ub.py b/src/diffcalc_api/routes/ub.py index b305020..49b5fb8 100644 --- a/src/diffcalc_api/routes/ub.py +++ b/src/diffcalc_api/routes/ub.py @@ -1,6 +1,6 @@ """Endpoints relating to the management of setting up the UB calculation.""" -from typing import Optional +from typing import List, Optional from fastapi import APIRouter, Body, Depends, Query @@ -12,14 +12,22 @@ NoTagOrIdxProvidedError, ) from diffcalc_api.examples import ub as examples -from diffcalc_api.models.response import InfoResponse, StringResponse +from diffcalc_api.models.response import ( + ArrayResponse, + InfoResponse, + MiscutResponse, + StringResponse, +) from diffcalc_api.models.ub import ( AddOrientationParams, AddReflectionParams, EditOrientationParams, EditReflectionParams, HklModel, + MiscutModel, + PositionModel, SetLatticeParams, + XyzModel, select_idx_or_tag_str, ) from diffcalc_api.services import ub as service @@ -269,6 +277,168 @@ async def set_lattice( ) +@router.put("/{name}/miscut", response_model=InfoResponse) +async def set_miscut( + name: str, + rot_axis: XyzModel = Body(...), + angle: float = Query(...), + add_miscut: bool = Query(default=False), + store: HklCalcStore = Depends(get_store), + collection: Optional[str] = Query(default=None, example="B07"), +): + """Find the U matrix using a miscut axis/angle, and set this as the new U matrix. + + Args: + name: the name of the hkl object to access within the store + rot_axis: the rotational axis of the miscut + angle: the miscut angle + add_miscut: boolean determining extra processing on U matrix before it is set + store: accessor to the hkl object + collection: collection within which the hkl object resides + """ + await service.set_miscut(name, rot_axis, angle, add_miscut, store, collection) + return InfoResponse( + message=( + "Miscut has been set using the provided rotation axis and angle of the " + + f"miscut for crystal {name} of collection {collection}." + ) + ) + + +@router.get("/{name}/miscut", response_model=MiscutResponse) +async def get_miscut( + name: str, + store: HklCalcStore = Depends(get_store), + collection: Optional[str] = Query(default=None, example="B07"), +): + """Get the rotation axis and angle of the miscut, using current UB matrix. + + 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 + + Returns: + miscut angle and miscut axis as a list. + """ + angle, axis = await service.get_miscut(name, store, collection) + return MiscutResponse( + payload=MiscutModel( + angle=angle, rotation_axis=XyzModel(x=axis[0], y=axis[1], z=axis[2]) + ) + ) + + +@router.get("/{name}/miscut/hkl", response_model=MiscutResponse) +async def get_miscut_from_hkl( + name: str, + hkl: HklModel = Depends(), + pos: PositionModel = Depends(), + store: HklCalcStore = Depends(get_store), + collection: Optional[str] = Query(default=None, example="B07"), +): + """Get the rotation axis and angle of the miscut using a single reflection. + + Args: + name: the name of the hkl object to access within the store + hkl: hkl of the reflection + pos: position of the reflection + store: accessor to the hkl object + collection: collection within which the hkl object resides + + Returns: + miscut angle and miscut axis as a tuple. + """ + angle, axis = await service.get_miscut_from_hkl(name, hkl, pos, store, collection) + return MiscutResponse( + payload=MiscutModel( + angle=angle, rotation_axis=XyzModel(x=axis[0], y=axis[1], z=axis[2]) + ) + ) + + +@router.get("/{name}/ub", response_model=ArrayResponse) +async def calculate_ub( + name: str, + tag1: Optional[str] = Query(default=None, example="refl1"), + idx1: Optional[int] = Query(default=None), + tag2: Optional[str] = Query(default=None, example="plane"), + idx2: Optional[int] = Query(default=None), + store: HklCalcStore = Depends(get_store), + collection: Optional[str] = Query(default=None, example="B07"), +): + """Calculate the UB matrix. + + 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. + tag1: the tag of the first reference object. + idx1: the index of the first reference object. + tag2: the tag of the second reference object. + idx2: the index of the second reference object. + + For each reference object, only a tag or index needs to be given. If none are + provided, diffcalc-core tries to work it out from the available reference + objects. + + Returns: + ArrayResponse object containing a list of angles, combined together into one + dictionary. + + """ + content = await service.calculate_ub( + name, store, collection, tag1, idx1, tag2, idx2 + ) + return ArrayResponse(payload=content) + + +@router.put("/{name}/ub", response_model=InfoResponse) +async def set_ub( + name: str, + ub_matrix: List[List[float]] = Body( + ..., example=[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]] + ), + store: HklCalcStore = Depends(get_store), + collection: Optional[str] = Query(default=None, example="B07"), +): + """Manually set the UB matrix. + + Args: + name: the name of the hkl object to access within the store + u_matrix: 3d array containing the UB matrix + store: accessor to the hkl object. + collection: collection within which the hkl object resides. + """ + await service.set_u(name, ub_matrix, store, collection) + return InfoResponse( + payload=f"UB matrix set for crystal {name} of collection {collection}" + ) + + +@router.put("/{name}/u", response_model=InfoResponse) +async def set_u( + name: str, + u_matrix: List[List[float]] = Body( + ..., example=[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]] + ), + store: HklCalcStore = Depends(get_store), + collection: Optional[str] = Query(default=None, example="B07"), +): + """Manually set the U matrix. + + Args: + name: the name of the hkl object to access within the store + u_matrix: 3d array containing the U matrix + store: accessor to the hkl object. + collection: collection within which the hkl object resides. + """ + await service.set_u(name, u_matrix, store, collection) + return InfoResponse( + payload=f"U matrix set for crystal {name} of collection {collection}" + ) + + @router.put("/{name}/{property}", response_model=InfoResponse) async def modify_property( name: str, diff --git a/src/diffcalc_api/services/hkl.py b/src/diffcalc_api/services/hkl.py index 3661cab..512db17 100644 --- a/src/diffcalc_api/services/hkl.py +++ b/src/diffcalc_api/services/hkl.py @@ -1,7 +1,7 @@ """Defines business logic for handling requests from hkl endpoints.""" from itertools import product -from typing import Dict, List, Optional, Tuple, Union +from typing import Dict, List, Optional, Tuple import numpy as np from diffcalc.hkl.geometry import Position @@ -265,42 +265,3 @@ def combine_lab_position_results( result.append({**physical_angles.asdict, **virtual_angles}) return result - - -async def calculate_ub( - name: str, - store: HklCalcStore, - collection: Optional[str], - tag1: Optional[str], - idx1: Optional[int], - tag2: Optional[str], - idx2: Optional[int], -) -> List[List[float]]: - """Calculate the UB matrix. - - 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. - tag1: the tag of the first reference object. - idx1: the index of the first reference object. - tag2: the tag of the second reference object. - idx2: the index of the second reference object. - - For each reference object, only a tag or index needs to be given. If none are - provided, diffcalc-core tries to work it out from the available reference - objects. - - Returns: - a list of angles, combined together into one dictionary. - - """ - hklcalc = await store.load(name, collection) - - first_retrieve: Optional[Union[str, int]] = tag1 if tag1 else idx1 - second_retrieve: Optional[Union[str, int]] = tag2 if tag2 else idx2 - - hklcalc.ubcalc.calc_ub(first_retrieve, second_retrieve) - - await store.save(name, hklcalc, collection) - return np.round(hklcalc.ubcalc.UB, 6).tolist() diff --git a/src/diffcalc_api/services/ub.py b/src/diffcalc_api/services/ub.py index ed85aa9..d9499b4 100644 --- a/src/diffcalc_api/services/ub.py +++ b/src/diffcalc_api/services/ub.py @@ -1,17 +1,21 @@ """Business logic for handling requests from ub endpoints.""" -from typing import Optional, Union +from typing import List, Optional, Tuple, Union +import numpy as np from diffcalc.hkl.geometry import Position +from diffcalc.ub.calc import UBCalculation -from diffcalc_api.errors.ub import ReferenceRetrievalError +from diffcalc_api.errors.ub import NoUbMatrixError, ReferenceRetrievalError from diffcalc_api.models.ub import ( AddOrientationParams, AddReflectionParams, EditOrientationParams, EditReflectionParams, HklModel, + PositionModel, SetLatticeParams, + XyzModel, ) from diffcalc_api.stores.protocol import HklCalcStore @@ -307,3 +311,170 @@ async def modify_property( setattr(hklcalc.ubcalc, property, tuple(target_value.dict().values())) await store.save(name, hklcalc, collection) + + +async def set_miscut( + name: str, + rot_axis: XyzModel, + angle: float, + add_miscut: bool, + store: HklCalcStore, + collection: Optional[str], +) -> None: + """Find the U matrix using a miscut axis/angle, and set this as the new U matrix. + + Args: + name: the name of the hkl object to access within the store + rot_axis: the rotational axis of the miscut + angle: the miscut angle + add_miscut: boolean determining extra processing on U matrix before it is set + store: accessor to the hkl object + collection: collection within which the hkl object resides + """ + hklcalc = await store.load(name, collection) + + ubcalc: UBCalculation = hklcalc.ubcalc + ubcalc.set_miscut((rot_axis.x, rot_axis.y, rot_axis.z), angle, add_miscut) + + await store.save(name, hklcalc, collection) + + +async def get_miscut( + name: str, + store: HklCalcStore, + collection: Optional[str], +) -> Tuple[float, List[float]]: + """Get the rotation axis and angle of the miscut, using current UB matrix. + + 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 + + Returns: + miscut angle and miscut axis as a list. + """ + hklcalc = await store.load(name, collection) + + ubcalc: UBCalculation = hklcalc.ubcalc + try: + angle, axis = ubcalc.get_miscut() + except ValueError: + raise NoUbMatrixError() + + return angle, axis.T.tolist()[0] + + +async def get_miscut_from_hkl( + name: str, + hkl: HklModel, + pos: PositionModel, + store: HklCalcStore, + collection: Optional[str], +) -> Tuple[float, Tuple[float, float, float]]: + """Get the rotation axis and angle of the miscut using a single reflection. + + Args: + name: the name of the hkl object to access within the store + hkl: hkl of the reflection + pos: position of the reflection + store: accessor to the hkl object + collection: collection within which the hkl object resides + + Returns: + miscut angle and miscut axis as a tuple. + """ + hklcalc = await store.load(name, collection) + + ubcalc: UBCalculation = hklcalc.ubcalc + try: + angle, axis = ubcalc.get_miscut_from_hkl( + (hkl.h, hkl.k, hkl.l), Position(**pos.dict()) + ) + except ValueError: + raise NoUbMatrixError() + + return angle, axis + + +async def calculate_ub( + name: str, + store: HklCalcStore, + collection: Optional[str], + tag1: Optional[str], + idx1: Optional[int], + tag2: Optional[str], + idx2: Optional[int], +) -> List[List[float]]: + """Calculate the UB matrix. + + 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. + tag1: the tag of the first reference object. + idx1: the index of the first reference object. + tag2: the tag of the second reference object. + idx2: the index of the second reference object. + + For each reference object, only a tag or index needs to be given. If none are + provided, diffcalc-core tries to work it out from the available reference + objects. + + Returns: + 3x3 UB matrix in list form + + """ + hklcalc = await store.load(name, collection) + + first_retrieve: Optional[Union[str, int]] = tag1 if tag1 else idx1 + second_retrieve: Optional[Union[str, int]] = tag2 if tag2 else idx2 + + hklcalc.ubcalc.calc_ub(first_retrieve, second_retrieve) + + await store.save(name, hklcalc, collection) + return np.round(hklcalc.ubcalc.UB, 6).tolist() + + +async def set_u( + name: str, + u_matrix: List[List[float]], + store: HklCalcStore, + collection: Optional[str], +): + """Manually set the U matrix. + + Args: + name: the name of the hkl object to access within the store + u_matrix: 3d array containing the U matrix + store: accessor to the hkl object. + collection: collection within which the hkl object resides. + """ + hklcalc = await store.load(name, collection) + + ubcalc: UBCalculation = hklcalc.ubcalc + ubcalc.set_u(u_matrix) + + await store.save(name, hklcalc, collection) + + +async def set_ub( + name: str, + ub_matrix: List[List[float]], + store: HklCalcStore, + collection: Optional[str], +): + """Manually set the UB matrix. + + Args: + name: the name of the hkl object to access within the store + ub_matrix: 3d array containing the UB matrix + store: accessor to the hkl object. + collection: collection within which the hkl object resides. + """ + hklcalc = await store.load(name, collection) + + ubcalc: UBCalculation = hklcalc.ubcalc + ubcalc.set_ub(ub_matrix) + + await store.save(name, hklcalc, collection) diff --git a/tests/test_hklcalc.py b/tests/test_hklcalc.py index bca6258..5603af6 100644 --- a/tests/test_hklcalc.py +++ b/tests/test_hklcalc.py @@ -229,27 +229,3 @@ def test_invalid_scans(client: TestClient): ) assert invalid_wavelength_scan.status_code == ErrorCodes.INVALID_SCAN_BOUNDS - - -def test_calc_ub(client: TestClient): - response = client.get( - "/hkl/test/UB", params={"first_tag": "refl1", "second_tag": "plane"} - ) - expected_ub = [ - [ - 1.27889, - -0.0, - 0.0, - ], - [-0.0, 1.278111, 0.04057], - [-0.0, -0.044633, 1.161768], - ] - - assert response.status_code == 200 - assert ast.literal_eval(response.content.decode())["payload"] == expected_ub - - -def test_calc_ub_fails_when_incorrect_tags(client: TestClient): - response = client.get("/hkl/test/UB", params={"tag1": "one", "idx2": 2}) - - assert response.status_code == 400 diff --git a/tests/test_ubcalc.py b/tests/test_ubcalc.py index 57399ed..52ebbdd 100644 --- a/tests/test_ubcalc.py +++ b/tests/test_ubcalc.py @@ -1,4 +1,5 @@ import ast +from ast import literal_eval import numpy as np import pytest @@ -8,28 +9,33 @@ from diffcalc.ub.calc import UBCalculation from fastapi.testclient import TestClient -from diffcalc_api.errors.ub import ErrorCodes +from diffcalc_api.errors.ub import ErrorCodes, NoUbMatrixError from diffcalc_api.server import app -from diffcalc_api.stores.protocol import HklCalcStore, get_store +from diffcalc_api.stores.protocol import get_store from tests.conftest import FakeHklCalcStore -dummy_hkl = HklCalculation(UBCalculation(name="dummy"), Constraints()) +class Client: + def __init__(self, hkl): + self.hkl = hkl -def dummy_get_store() -> HklCalcStore: - return FakeHklCalcStore(dummy_hkl) + @property + def client(self): + app.dependency_overrides[get_store] = lambda: FakeHklCalcStore(self.hkl) + return TestClient(app) -@pytest.fixture(scope="session") -def client() -> TestClient: - app.dependency_overrides[get_store] = dummy_get_store +@pytest.fixture +def client_generator(request) -> Client: + """Create tester object""" + return Client(request.param) - return TestClient(app) +def test_get_ub(): + hkl = HklCalculation(UBCalculation("dummy"), Constraints()) + client = Client(hkl).client -def test_get_ub(client: TestClient): response = client.get("/ub/test") - assert ast.literal_eval(response.content.decode())["payload"] == ( "UBCALC\n\n" + " name: dummy" @@ -51,29 +57,42 @@ def test_get_ub(client: TestClient): assert response.status_code == 200 -def test_add_reflection(client: TestClient): +def test_add_reflection(): + ubcalc = UBCalculation() + hkl = HklCalculation(ubcalc, Constraints()) + client = Client(hkl).client + response = client.post( "/ub/test/reflection?tag=foo", json={ "hkl": {"h": 0, "k": 0, "l": 1}, - "position": {"mu": 7, "delta": 0, "nu": 10, "eta": 0, "chi": 0, "phi": 0}, + "position": { + "mu": 7, + "delta": 0, + "nu": 10, + "eta": 0, + "chi": 0, + "phi": 0, + }, "energy": 12, }, ) assert response.status_code == 200 - assert dummy_hkl.ubcalc.get_reflection("foo") + assert ubcalc.get_reflection("foo") - dummy_hkl.ubcalc.del_reflection("foo") +def test_edit_reflection(): + ubcalc = UBCalculation() + hkl = HklCalculation(ubcalc, Constraints()) + client = Client(hkl).client -def test_edit_reflection(client: TestClient): - dummy_hkl.ubcalc.add_reflection([0, 0, 1], Position(7, 0, 10, 0, 0, 0), 12, "foo") + ubcalc.add_reflection([0, 0, 1], Position(7, 0, 10, 0, 0, 0), 12, "foo") response = client.put( "/ub/test/reflection?tag=foo", json={"energy": 13, "set_tag": "bar"}, ) - reflection = dummy_hkl.ubcalc.get_reflection("bar") + reflection = ubcalc.get_reflection("bar") assert response.status_code == 200 assert reflection.energy == 13 @@ -84,27 +103,30 @@ def test_edit_reflection(client: TestClient): ) assert response_idx.status_code == 200 - reflection_idx = dummy_hkl.ubcalc.get_reflection(0) + reflection_idx = ubcalc.get_reflection(0) assert reflection_idx.h == 0 assert reflection_idx.k == 3 assert reflection_idx.tag == "bar" - dummy_hkl.ubcalc.del_reflection(0) +def test_delete_reflection(): + ubcalc = UBCalculation() + hkl = HklCalculation(ubcalc, Constraints()) + client = Client(hkl).client -def test_delete_reflection(client: TestClient): - dummy_hkl.ubcalc.add_reflection([0, 0, 1], Position(7, 0, 10, 0, 0, 0), 12, "foo") + ubcalc.add_reflection([0, 0, 1], Position(7, 0, 10, 0, 0, 0), 12, "foo") response = client.delete("/ub/test/reflection?tag=foo") assert response.status_code == 200 with pytest.raises(Exception): - dummy_hkl.ubcalc.get_reflection("foo") + ubcalc.get_reflection("foo") -def test_edit_or_delete_reflection_fails_for_non_existing_reflection( - client: TestClient, -): +def test_edit_or_delete_reflection_fails_for_non_existing_reflection(): + hkl = HklCalculation(UBCalculation(), Constraints()) + client = Client(hkl).client + edit_response = client.put( "/ub/test/reflection?tag=foo", json={"energy": 13}, @@ -115,7 +137,11 @@ def test_edit_or_delete_reflection_fails_for_non_existing_reflection( assert delete_response.status_code == ErrorCodes.REFERENCE_RETRIEVAL_ERROR -def test_add_orientation(client: TestClient): +def test_add_orientation(): + ubcalc = UBCalculation() + hkl = HklCalculation(ubcalc, Constraints()) + client = Client(hkl).client + response = client.post( "/ub/test/orientation?tag=bar", json={ @@ -125,18 +151,20 @@ def test_add_orientation(client: TestClient): ) assert response.status_code == 200 - assert dummy_hkl.ubcalc.get_orientation("bar") + assert ubcalc.get_orientation("bar") - dummy_hkl.ubcalc.del_orientation("bar") +def test_edit_orientation(): + ubcalc = UBCalculation() + hkl = HklCalculation(ubcalc, Constraints()) + client = Client(hkl).client -def test_edit_orientation(client: TestClient): - dummy_hkl.ubcalc.add_orientation([0, 0, 1], [0, 0, 1], None, "bar") + ubcalc.add_orientation([0, 0, 1], [0, 0, 1], None, "bar") response = client.put( "/ub/test/orientation?tag=bar", json={"xyz": {"x": 1, "y": 1, "z": 0}, "set_tag": "bar"}, ) - orientation = dummy_hkl.ubcalc.get_orientation("bar") + orientation = ubcalc.get_orientation("bar") assert response.status_code == 200 @@ -144,21 +172,24 @@ def test_edit_orientation(client: TestClient): assert orientation.y == 1 assert orientation.z == 0 - dummy_hkl.ubcalc.del_orientation("bar") +def test_delete_orientation(): + ubcalc = UBCalculation() + hkl = HklCalculation(ubcalc, Constraints()) + client = Client(hkl).client -def test_delete_orientation(client: TestClient): - dummy_hkl.ubcalc.add_orientation([0, 0, 1], [0, 0, 1], None, "bar") + ubcalc.add_orientation([0, 0, 1], [0, 0, 1], None, "bar") response = client.delete("/ub/test/orientation?idx=0") assert response.status_code == 200 with pytest.raises(Exception): - dummy_hkl.ubcalc.get_orientation("bar") + ubcalc.get_orientation("bar") -def test_edit_or_delete_orientation_fails_for_non_existing_orientation( - client: TestClient, -): +def test_edit_or_delete_orientation_fails_for_non_existing_orientation(): + hkl = HklCalculation(UBCalculation(), Constraints()) + client = Client(hkl).client + edit_response = client.put( "/ub/test/orientation?tag=bar", json={"xyz": {"x": 1, "y": 1, "z": 0}}, @@ -171,17 +202,23 @@ def test_edit_or_delete_orientation_fails_for_non_existing_orientation( assert delete_response.status_code == ErrorCodes.REFERENCE_RETRIEVAL_ERROR -def test_set_lattice(client: TestClient): +def test_set_lattice(): + ubcalc = UBCalculation() + hkl = HklCalculation(ubcalc, Constraints()) + client = Client(hkl).client + response = client.patch( "/ub/test/lattice", json={"a": 2}, ) assert response.status_code == 200 - assert dummy_hkl.ubcalc.crystal + assert ubcalc.crystal -def test_set_lattice_fails_for_empty_data(client: TestClient): +def test_set_lattice_fails_for_empty_data(): + hkl = HklCalculation(UBCalculation(), Constraints()) + client = Client(hkl).client response_with_no_input = client.patch( "/ub/test/lattice", json={}, @@ -198,17 +235,184 @@ def test_set_lattice_fails_for_empty_data(client: TestClient): assert response_with_no_input.status_code == ErrorCodes.INVALID_SET_LATTICE_PARAMS -def test_modify_property(client: TestClient): +def test_get_miscut(): + ubcalc = UBCalculation() + hkl = HklCalculation(ubcalc, Constraints()) + client = Client(hkl).client + + ubcalc.add_reflection( + (0, 0, 1), Position(7.31, 0, 10.62, 0, 0, 0), 12.39842, "refl" + ) + ubcalc.add_orientation((0, 1, 0), (0, 1, 0), tag="plane") + + ubcalc.set_lattice("", 4.913, 5.405) + ubcalc.calc_ub("refl", "plane") + response = client.get("/ub/test/miscut?collection=B07") + + response_dict = literal_eval(response.content.decode())["payload"] + + assert response_dict["rotation_axis"]["x"] == pytest.approx(-1.0) + assert response_dict["rotation_axis"]["y"] == pytest.approx(0) + assert response_dict["rotation_axis"]["z"] == pytest.approx(0) + + +def test_get_miscut_fails_when_no_ub_matrix(): + hkl = HklCalculation(UBCalculation(), Constraints()) + client = Client(hkl).client + + response = client.get("/ub/test/miscut?collection=B07") + + response_with_hkl = client.get( + "/ub/test/miscut/hkl?collection=B07", + params={ + "h": 1, + "k": 0, + "l": 1, + "mu": 45, + "delta": 90, + "nu": 15, + "eta": 20, + "chi": 90, + "phi": 35, + }, + ) + + assert response.status_code == ErrorCodes.NO_UB_MATRIX_ERROR + assert response_with_hkl.status_code == ErrorCodes.NO_UB_MATRIX_ERROR + assert ( + literal_eval(response.content.decode())["message"] == NoUbMatrixError().detail + ) + assert ( + literal_eval(response_with_hkl.content.decode())["message"] + == NoUbMatrixError().detail + ) + + +def test_get_miscut_from_hkl(): + ubcalc = UBCalculation() + hkl = HklCalculation(ubcalc, Constraints()) + client = Client(hkl).client + + ubcalc.add_reflection( + (0, 0, 1), Position(7.31, 0, 10.62, 0, 0, 0), 12.39842, "refl" + ) + ubcalc.add_orientation((0, 1, 0), (0, 1, 0), tag="plane") + ubcalc.set_lattice("", 4.913, 5.405) + ubcalc.calc_ub("refl", "plane") + + response = client.get( + "/ub/test/miscut/hkl?collection=B07", + params={ + "h": 0, + "k": 0, + "l": 1, + "mu": 90, + "delta": 90, + "nu": 90, + "eta": 90, + "chi": 90, + "phi": 90, + }, + ) + + assert response.status_code == 200 + + +def test_set_miscut(): + ubcalc = UBCalculation() + hkl = HklCalculation(ubcalc, Constraints()) + client = Client(hkl).client + assert ubcalc.U is None + + response = client.put( + "/ub/test/miscut?collection=B07", + params={"angle": 0.035, "add_miscut": False}, + json={"x": -1, "y": 0, "z": 0}, + ) + + assert response.status_code == 200 + assert ubcalc.U is not None + + +def test_calc_ub(): + ubcalc = UBCalculation() + hkl = HklCalculation(ubcalc, Constraints()) + client = Client(hkl).client + + ubcalc.set_lattice("SiO2", 4.913, 5.405) + ubcalc.n_hkl = (1, 0, 0) + ubcalc.add_reflection( + (0, 0, 1), Position(7.31, 0, 10.62, 0, 0, 0), 12.39842, "refl1" + ) + ubcalc.add_orientation((0, 1, 0), (0, 1, 0), None, "plane") + ubcalc.calc_ub("refl1", "plane") + + response = client.get( + "/ub/test/ub", params={"first_tag": "refl1", "second_tag": "plane"} + ) + expected_ub = [ + [ + 1.27889, + -0.0, + 0.0, + ], + [-0.0, 1.278111, 0.04057], + [-0.0, -0.044633, 1.161768], + ] + + assert response.status_code == 200 + assert ast.literal_eval(response.content.decode())["payload"] == expected_ub + + +def test_calc_ub_fails_when_incorrect_tags(): + ubcalc = UBCalculation() + hkl = HklCalculation(ubcalc, Constraints()) + client = Client(hkl).client + + ubcalc.set_lattice("SiO2", 4.913, 5.405) + response = client.get("/ub/test/ub", params={"tag1": "one", "idx2": 2}) + + assert response.status_code == 400 + + +def test_set_u(): + ubcalc = UBCalculation() + hkl = HklCalculation(ubcalc, Constraints()) + client = Client(hkl).client + + u_matrix = np.identity(3) + client.put("/ub/test/u?collection=B07", json=u_matrix.tolist()) + + assert np.all(ubcalc.U == u_matrix) + + +def test_set_ub(): + ubcalc = UBCalculation() + hkl = HklCalculation(ubcalc, Constraints()) + client = Client(hkl).client + + ub_matrix = np.identity(3) + client.put("/ub/test/ub?collection=B07", json=ub_matrix.tolist()) + + assert np.all(ubcalc.U == ub_matrix) + + +def test_modify_property(): + ubcalc = UBCalculation() + hkl = HklCalculation(ubcalc, Constraints()) + client = Client(hkl).client response = client.put( "/ub/test/n_hkl", json={"h": 0, "k": 0, "l": 1}, ) assert response.status_code == 200 - assert np.all(dummy_hkl.ubcalc.n_hkl == np.transpose([[0, 0, 1]])) + assert np.all(ubcalc.n_hkl == np.transpose([[0, 0, 1]])) -def test_modify_non_existent_property(client: TestClient): +def test_modify_non_existent_property(): + hkl = HklCalculation(UBCalculation(), Constraints()) + client = Client(hkl).client response = client.put( "/ub/test/silly_property", json={"h": 0, "k": 0, "l": 1},