Skip to content
This repository was archived by the owner on May 7, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/code.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ jobs:
if: ${{ !matrix.deploy }}
run: |
python -m pip install --upgrade pip
pip install .[dev]
pip install .[dev] -r requirements.txt

- name: Install dependencies (deploy)
if: ${{ matrix.deploy }}
Expand Down
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"--reload"
],
"jinja": true,
"justMyCode": true
"justMyCode": false
},
{
"name": "Debug Unit Test",
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ certifi==2022.9.24
cfgv==3.3.1
click==8.1.3
coverage==6.5.0
git+https://github.com/DiamondLightSource/diffcalc-core.git#egg=diffcalc-core
git+https://github.com/DiamondLightSource/diffcalc-core@addExtraFunctions#egg=diffcalc-core
distlib==0.3.6
docutils==0.19
fastapi==0.86.0
Expand Down
30 changes: 30 additions & 0 deletions src/diffcalc_api/errors/ub.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ class ErrorCodes(ErrorCodesBase):
NO_TAG_OR_IDX_PROVIDED = 400
BOTH_TAG_OR_IDX_PROVIDED = 400
NO_UB_MATRIX_ERROR = 400
NO_CRYSTAL_ERROR = 400
INVALID_INDEX_ERROR = 400


responses = {code: ALL_RESPONSES[code] for code in np.unique(ErrorCodes.all_codes())}
Expand Down Expand Up @@ -98,3 +100,31 @@ def __init__(self, message: Optional[str] = None):
else message
)
self.status_code = ErrorCodes.NO_UB_MATRIX_ERROR


class NoCrystalError(DiffcalcAPIException):
"""When there is no crystal lattice set, some commands in diffcalc-core fail."""

def __init__(self, message: Optional[str] = None):
"""Set detail and status code."""
self.detail = (
(
"It seems like there is no crystal lattice set for this record. Please"
+ " try again after setting the lattice."
)
if message is None
else message
)
self.status_code = ErrorCodes.NO_UB_MATRIX_ERROR


class InvalidIndexError(DiffcalcAPIException):
"""Error gets thrown if an invalid miller index name is given."""

def __init__(self, index: str):
"""Set detail and status code."""
self.detail = (
"Index supplied must be one of {h, k, l}. " + f"Found {index} instead."
)

self.status_code = ErrorCodes.INVALID_INDEX_ERROR
14 changes: 7 additions & 7 deletions src/diffcalc_api/models/response.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
"""Pydantic models relating to all endpoint responses."""
from typing import Dict, List
from typing import Dict, List, Union

from pydantic import BaseModel

from diffcalc_api.models.ub import HklModel, MiscutModel
from diffcalc_api.models.ub import HklModel, MiscutModel, SphericalCoordinates, XyzModel


class InfoResponse(BaseModel):
Expand Down Expand Up @@ -46,14 +46,14 @@ class DiffractorAnglesResponse(BaseModel):
payload: List[Dict[str, float]]


class MillerIndicesResponse(BaseModel):
"""Miller Indices Response.
class CoordinateResponse(BaseModel):
"""Coordinate response model.

Used for any endpoint with an attached service that returns a set of miller
indices.
Returns coordinates, in three dimensions, for a given coordinate system.
Supported systems include spherical coordinates, reciprocal and real space.
"""

payload: HklModel
payload: Union[SphericalCoordinates, HklModel, XyzModel]


class MiscutResponse(BaseModel):
Expand Down
11 changes: 11 additions & 0 deletions src/diffcalc_api/models/ub.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,14 @@ class MiscutModel(BaseModel):

angle: float
rotation_axis: XyzModel


class SphericalCoordinates(BaseModel):
"""Data storing a point in the spherical coordinate frame.

Requires an azimuthal and polar angle, as well as a magnitude.
"""

magnitude: float
azimuth_angle: float
polar_angle: float
8 changes: 4 additions & 4 deletions src/diffcalc_api/routes/hkl.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
from diffcalc_api.errors.hkl import InvalidSolutionBoundsError
from diffcalc_api.models.hkl import SolutionConstraints
from diffcalc_api.models.response import (
CoordinateResponse,
DiffractorAnglesResponse,
MillerIndicesResponse,
ScanResponse,
)
from diffcalc_api.models.ub import HklModel, PositionModel
Expand Down Expand Up @@ -60,7 +60,7 @@ async def lab_position_from_miller_indices(
return DiffractorAnglesResponse(payload=positions)


@router.get("/{name}/position/hkl", response_model=MillerIndicesResponse)
@router.get("/{name}/position/hkl", response_model=CoordinateResponse)
async def miller_indices_from_lab_position(
name: str,
pos: PositionModel = Depends(),
Expand All @@ -78,12 +78,12 @@ async def miller_indices_from_lab_position(
collection: collection within which the hkl object resides.

Returns:
MillerIndicesResponse containing the miller indices.
CoordinateResponse containing the miller indices.
"""
hkl = await service.miller_indices_from_lab_position(
name, pos, wavelength, store, collection
)
return MillerIndicesResponse(payload=hkl)
return CoordinateResponse(payload=hkl)


@router.get("/{name}/scan/hkl", response_model=ScanResponse)
Expand Down
150 changes: 144 additions & 6 deletions src/diffcalc_api/routes/ub.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from diffcalc_api.examples import ub as examples
from diffcalc_api.models.response import (
ArrayResponse,
CoordinateResponse,
InfoResponse,
MiscutResponse,
StringResponse,
Expand All @@ -25,6 +26,7 @@
MiscutModel,
PositionModel,
SetLatticeParams,
SphericalCoordinates,
XyzModel,
select_idx_or_tag_str,
)
Expand Down Expand Up @@ -534,7 +536,7 @@ async def set_lab_reference_vector(
)


@router.put("/{name}/nhkl")
@router.put("/{name}/nhkl", response_model=InfoResponse)
async def set_miller_reference_vector(
name: str,
target_value: HklModel = Body(..., example={"h": 1, "k": 0, "l": 0}),
Expand All @@ -559,7 +561,7 @@ async def set_miller_reference_vector(
)


@router.put("/{name}/surface/nphi")
@router.put("/{name}/surface/nphi", response_model=InfoResponse)
async def set_lab_surface_normal(
name: str,
target_value: XyzModel = Body(..., example={"x": 1, "y": 0, "z": 0}),
Expand All @@ -584,7 +586,7 @@ async def set_lab_surface_normal(
)


@router.put("/{name}/surface/nhkl")
@router.put("/{name}/surface/nhkl", response_model=InfoResponse)
async def set_miller_surface_normal(
name: str,
target_value: HklModel = Body(..., example={"h": 1, "k": 0, "l": 0}),
Expand Down Expand Up @@ -636,7 +638,7 @@ async def get_lab_reference_vector(
return InfoResponse(message="This vector does not exist.")


@router.get("/{name}/nhkl")
@router.get("/{name}/nhkl", response_model=Union[ArrayResponse, InfoResponse])
async def get_miller_reference_vector(
name: str,
store: HklCalcStore = Depends(get_store),
Expand All @@ -663,7 +665,7 @@ async def get_miller_reference_vector(
return InfoResponse(message="This vector does not exist.")


@router.get("/{name}/surface/nphi")
@router.get("/{name}/surface/nphi", response_model=Union[ArrayResponse, InfoResponse])
async def get_lab_surface_normal(
name: str,
store: HklCalcStore = Depends(get_store),
Expand All @@ -690,7 +692,7 @@ async def get_lab_surface_normal(
return InfoResponse(message="This vector does not exist.")


@router.get("/{name}/surface/nhkl")
@router.get("/{name}/surface/nhkl", response_model=Union[ArrayResponse, InfoResponse])
async def get_miller_surface_normal(
name: str,
store: HklCalcStore = Depends(get_store),
Expand All @@ -715,3 +717,139 @@ async def get_miller_surface_normal(
return ArrayResponse(payload=lab_vector)
else:
return InfoResponse(message="This vector does not exist.")


#######################################################################################
# Vector Calculations in HKL Space #
#######################################################################################


@router.get("/{name}/vector", response_model=CoordinateResponse)
async def calculate_vector_from_hkl_and_offset(
name: str,
hkl_ref: HklModel = Depends(),
polar_angle: float = Query(..., example=45.0),
azimuth_angle: float = Query(..., example=45.0),
store: HklCalcStore = Depends(get_store),
collection: Optional[str] = Query(default=None, example="B07"),
):
"""Calculate a vector in reciprocal space relative to a reference vector.

Note, this method requires that a UB matrix exists for the Hkl object retrieved.

Args:
name: the name of the hkl object to access within the store
hkl_ref: the reference vector in hkl space
polar_angle: the polar angle, or the inclination between the zenith and
reference vector.
azimuth_angle: the azimuth angle
store: accessor to the hkl object.
collection: collection within which the hkl object resides.

Returns:
CoordinateResponse
Containing the calculated reciprocal space vector as h, k, l indices.

"""
vector = await service.calculate_vector_from_hkl_and_offset(
name, hkl_ref, polar_angle, azimuth_angle, store, collection
)

return CoordinateResponse(payload=HklModel(h=vector[0], k=vector[1], l=vector[2]))


@router.get("/{name}/offset", response_model=CoordinateResponse)
async def calculate_offset_from_vector_and_hkl(
name: str,
h1: float = Query(..., example=0.0),
k1: float = Query(..., example=1.0),
l1: float = Query(..., example=0.0),
h2: float = Query(..., example=1.0),
k2: float = Query(..., example=0.0),
l2: float = Query(..., example=0.0),
store: HklCalcStore = Depends(get_store),
collection: Optional[str] = Query(default=None, example="B07"),
):
"""Calculate angles and magnitude differences between two reciprocal space vectors.

Note, this method requires that a UB matrix exists for the Hkl object retrieved,
and that a lattice has been set for it.

Args:
name: the name of the hkl object to access within the store
h1: h index of the reference vector
k1: k index of the reference vector
l1: l index of the reference vector
h2: h index of the vector relative to which the offset should be calculated
k2: k index of the vector relative to which the offset should be calculated
l2: l index of the vector relative to which the offset should be calculated
store: accessor to the hkl object.
collection: collection within which the hkl object resides.

Returns:
CoordinateResponse
The offset, in spherical coordinates, between the two reciprocal space vectors,
containing the polar angle, azimuth angle and magnitude between them.

"""
hkl_ref = HklModel(h=h1, k=k1, l=l1)
hkl_offset = HklModel(h=h2, k=k2, l=l2)

vector = await service.calculate_offset_from_vector_and_hkl(
name, hkl_offset, hkl_ref, store, collection
)

return CoordinateResponse(
payload=SphericalCoordinates(
polar_angle=vector[0], azimuth_angle=vector[1], magnitude=vector[2]
)
)


#######################################################################################
# HKL Solver For Fixed Scattering Vector #
#######################################################################################


@router.get("/{name}/solve/hkl/fixed/q", response_model=ArrayResponse)
async def hkl_solver_for_fixed_q(
name: str,
hkl: HklModel = Depends(),
index_name: str = Query(..., example="h"),
index_value: float = Query(..., example=0.0),
a: float = Query(..., example=0.0),
b: float = Query(..., example=1.0),
c: float = Query(..., example=0.0),
d: float = Query(..., example=0.25),
store: HklCalcStore = Depends(get_store),
collection: Optional[str] = Query(default=None, example="B07"),
):
"""Find valid hkl indices for a fixed scattering vector.

Note, this method requires that a UB matrix exists for the Hkl object retrieved.
Coefficients are used to constrain solutions as:
a*h + b*k + c*l = d

Args:
name: the name of the hkl object to access within the store.
hkl: Reciprocal space vector from which a scattering vector will be calculated.
index_name: Which miller index to set,
index_value: value of this miller index.
a: constraint on the hkl value.
b: constraint on the hkl value.
c: constraint on the hkl value.
d: constraint on the hkl value.
store: accessor to the hkl object.
collection: collection within which the hkl object resides.

Returns:
ArrayResponse
A list of lists, with each sublist being a solution.

"""
hkl_list = await service.hkl_solver_for_fixed_q(
name, hkl, index_name, index_value, a, b, c, d, store, collection
)
hkl_list_as_list_of_lists = [list(i) for i in hkl_list]

return ArrayResponse(payload=hkl_list_as_list_of_lists)
Loading