diff --git a/.azure-pipelines/ci.yml b/.azure-pipelines/ci.yml index 267c65e..1037099 100644 --- a/.azure-pipelines/ci.yml +++ b/.azure-pipelines/ci.yml @@ -31,6 +31,11 @@ steps: displayName: Run tests workingDirectory: $(Pipeline.Workspace)/src +- script: | + PYTHONDEVMODE=1 mypy src tests + displayName: Mypy + workingDirectory: $(Pipeline.Workspace) + - bash: bash <(curl -s https://codecov.io/bash) -n "Python $(PYTHON_VERSION) $(Agent.OS)" env: CODECOV_TOKEN: $(CODECOV_TOKEN) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..cbbf48c --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,21 @@ +# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.241.1/containers/python-3/.devcontainer/base.Dockerfile + +# [Choice] Python version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.10, 3.9, 3.8, 3.7, 3.6, 3-bullseye, 3.10-bullseye, 3.9-bullseye, 3.8-bullseye, 3.7-bullseye, 3.6-bullseye, 3-buster, 3.10-buster, 3.9-buster, 3.8-buster, 3.7-buster, 3.6-buster +ARG VARIANT="3.10-bullseye" +FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} + +# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 +ARG NODE_VERSION="none" +RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi + +# [Optional] If your pip requirements rarely change, uncomment this section to add them to the image. +# COPY requirements.txt /tmp/pip-tmp/ +# RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \ +# && rm -rf /tmp/pip-tmp + +# [Optional] Uncomment this section to install additional OS packages. +# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ +# && apt-get -y install --no-install-recommends + +# [Optional] Uncomment this line to install global node packages. +# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..3f4a575 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,54 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: +// https://github.com/microsoft/vscode-dev-containers/tree/v0.241.1/containers/python-3 +{ + "name": "Python 3", + "build": { + "dockerfile": "Dockerfile", + "context": "..", + "args": { + // Update 'VARIANT' to pick a Python version: 3, 3.10, 3.9, 3.8, 3.7, 3.6 + // Append -bullseye or -buster to pin to an OS version. + // Use -bullseye variants on local on arm64/Apple Silicon. + "VARIANT": "3.8", + // Options + "NODE_VERSION": "none" + } + }, + + // Configure tool-specific properties. + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + // Set *default* container specific settings.json values on container create. + "settings": { + "python.defaultInterpreterPath": "/usr/local/bin/python", + "python.linting.enabled": true, + "python.linting.pylintEnabled": true, + "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8", + "python.formatting.blackPath": "/usr/local/py-utils/bin/black", + "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf", + "python.linting.banditPath": "/usr/local/py-utils/bin/bandit", + "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8", + "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy", + "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle", + "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle", + "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint" + }, + + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "ms-python.python", + "ms-python.vscode-pylance" + ] + } + }, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "pip3 install --user -r requirements.txt", + + // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + // "remoteUser": "vscode" +} diff --git a/src/diffcalc/hkl/calc.py b/src/diffcalc/hkl/calc.py index 7b9babf..d1288a5 100644 --- a/src/diffcalc/hkl/calc.py +++ b/src/diffcalc/hkl/calc.py @@ -6,9 +6,10 @@ from copy import copy from itertools import product from math import acos, asin, atan, atan2, cos, degrees, isnan, pi, sin, sqrt, tan -from typing import Dict, Iterator, List, Optional, Tuple +from typing import Any, Dict, Iterator, List, Optional, Tuple import numpy as np +from diffcalc.hkl.constraints import Constraints from diffcalc.hkl.geometry import ( Position, get_rotation_matrices, @@ -18,6 +19,7 @@ rot_PHI, ) from diffcalc.log import logging +from diffcalc.ub.calc import UBCalculation from diffcalc.util import ( SMALL, DiffcalcException, @@ -508,8 +510,8 @@ def _calc_psi( else: sin_psi = cos(alpha) * sin(qaz - naz) sgn = sign(sin_tau) - eps = sin_psi ** 2 + cos_psi ** 2 - sigma_ = eps / sin_tau ** 2 - 1 + eps = sin_psi**2 + cos_psi**2 + sigma_ = eps / sin_tau**2 - 1 if not is_small(sigma_): print( "WARNING: Diffcalc could not calculate a unique azimuth " @@ -937,7 +939,7 @@ def __get_last_sample_angle(A: float, B: float, C: float) -> List[float]: "Sample orientation cannot be chosen uniquely. Please choose a different set of constraints." ) ks = atan2(A, B) - acos_alp = acos(bound(C / sqrt(A ** 2 + B ** 2))) + acos_alp = acos(bound(C / sqrt(A**2 + B**2))) if is_small(acos_alp): alp_list = [ ks, @@ -1513,7 +1515,7 @@ def _calc_sample_angles_given_two_sample_and_detector( acos_phi = acos( bound( (N_phi[2, 0] * cos(chi) - V20) - / (sin(chi) * sqrt(A ** 2 + B ** 2)) + / (sin(chi) * sqrt(A**2 + B**2)) ) ) except AssertionError: @@ -1562,7 +1564,7 @@ def _calc_sample_angles_given_two_sample_and_detector( try: acos_rhs = acos( bound( - (sin(qaz) * cos(theta) / cos(eta) - V) / sqrt(X ** 2 + Y ** 2) + (sin(qaz) * cos(theta) / cos(eta) - V) / sqrt(X**2 + Y**2) ) ) except AssertionError: @@ -1605,7 +1607,7 @@ def _calc_sample_angles_given_two_sample_and_detector( acos_V00 = acos( bound( (cos(theta) * sin(qaz) - N_phi[2, 0] * cos(eta) * sin(chi)) - / sqrt(A ** 2 + B ** 2) + / sqrt(A**2 + B**2) ) ) except AssertionError: @@ -1785,3 +1787,16 @@ def _verify_virtual_angles( "anglesToVirtualAngles of %f" % virtual_angles_readback[key] ) raise DiffcalcException(s) + + @property + def asdict(self) -> Dict[str, Any]: + return {"ubcalc": self.ubcalc.asdict, "constraints": self.constraints.asdict} + + @classmethod + def fromdict(cls, data: Dict[str, Any]) -> "HklCalculation": + constraint_data = data["constraints"] + indegrees = constraint_data.pop("indegrees") + return HklCalculation( + UBCalculation.fromdict(data["ubcalc"]), + Constraints(constraint_data, indegrees), + ) diff --git a/src/diffcalc/hkl/constraints.py b/src/diffcalc/hkl/constraints.py index ce8c41c..243dac3 100644 --- a/src/diffcalc/hkl/constraints.py +++ b/src/diffcalc/hkl/constraints.py @@ -20,6 +20,7 @@ class _Constraint: @property def active(self) -> bool: + # in the below statement, how can self.value ever be False? return self.value is not False and self.value is not None @@ -184,7 +185,11 @@ def asdict(self) -> Dict[str, Union[float, bool]]: Dict[str, Union[float, bool]] Dictionary with all constrained angle names and values. """ - return {con.name: getattr(self, con.name) for con in self._all if con.active} + con_dict = { + con.name: getattr(self, con.name) for con in self._all if con.active + } + con_dict["indegrees"] = self.indegrees + return con_dict @asdict.setter def asdict(self, constraints): diff --git a/src/diffcalc/hkl/geometry.py b/src/diffcalc/hkl/geometry.py index 438ce7d..7850ffa 100644 --- a/src/diffcalc/hkl/geometry.py +++ b/src/diffcalc/hkl/geometry.py @@ -8,24 +8,23 @@ .. [1] H. You. "Angle calculations for a '4S+2D' six-circle diffractometer" J. Appl. Cryst. (1999). 32, 614-623. """ +from dataclasses import dataclass from math import degrees, radians -from typing import Dict, Tuple, Union +from typing import Any, Tuple import numpy as np from diffcalc.util import I, x_rotation, y_rotation, z_rotation from numpy.linalg import inv +@dataclass class Position: """Class representing diffractometer orientation. Diffractometer orientation corresponding to (4+2) geometry - defined in H. You paper (add reference) Attributes ---------- - fields: Tuple[str, str, str, str, str, str] - Tuple with angle names mu: float, default = 0.0 mu angle value delta: float, default = 0.0 @@ -42,32 +41,23 @@ class Position: If True, arguments are angles in degrees. """ - fields: Tuple[str, str, str, str, str, str] = ( - "mu", - "delta", - "nu", - "eta", - "chi", - "phi", - ) - - def __init__( - self, - mu: float = 0.0, - delta: float = 0.0, - nu: float = 0.0, - eta: float = 0.0, - chi: float = 0.0, - phi: float = 0.0, - indegrees: bool = True, - ): - self._mu: float = radians(mu) if indegrees else mu - self._delta: float = radians(delta) if indegrees else delta - self._nu: float = radians(nu) if indegrees else nu - self._eta: float = radians(eta) if indegrees else eta - self._chi: float = radians(chi) if indegrees else chi - self._phi: float = radians(phi) if indegrees else phi - self.indegrees: bool = indegrees + mu: float = 0.0 + delta: float = 0.0 + nu: float = 0.0 + eta: float = 0.0 + chi: float = 0.0 + phi: float = 0.0 + indegrees: bool = True + + def __post_init__(self): + self.angles = { + "mu": self.mu, + "delta": self.delta, + "nu": self.nu, + "eta": self.eta, + "chi": self.chi, + "phi": self.phi, + } @classmethod def asdegrees(cls, pos: "Position") -> "Position": @@ -83,9 +73,12 @@ def asdegrees(cls, pos: "Position") -> "Position": Position New Position object with angles in degrees. """ - res = cls(**pos.asdict, indegrees=pos.indegrees) - res.indegrees = True - return res + if not pos.indegrees: + return cls( + **{key: degrees(value) for key, value in pos.angles.items()}, + indegrees=True + ) + return pos @classmethod def asradians(cls, pos: "Position") -> "Position": @@ -101,126 +94,15 @@ def asradians(cls, pos: "Position") -> "Position": Position New Position object with angles in radians. """ - res = cls(**pos.asdict, indegrees=pos.indegrees) - res.indegrees = False - return res - - @property - def mu(self) -> Union[float, None]: - """Value of of mu angle.""" - if self.indegrees: - return degrees(self._mu) - else: - return self._mu - - @mu.setter - def mu(self, val): - if self.indegrees: - self._mu = radians(val) - else: - self._mu = val - - @mu.deleter - def mu(self): - self._mu = None - - @property - def delta(self) -> Union[float, None]: - """Value of of delta angle.""" - if self.indegrees: - return degrees(self._delta) - else: - return self._delta - - @delta.setter - def delta(self, val): - if self.indegrees: - self._delta = radians(val) - else: - self._delta = val - - @delta.deleter - def delta(self): - self._delta = None - - @property - def nu(self) -> Union[float, None]: - """Value of of nu angle.""" - if self.indegrees: - return degrees(self._nu) - else: - return self._nu - - @nu.setter - def nu(self, val): - if self.indegrees: - self._nu = radians(val) - else: - self._nu = val - - @nu.deleter - def nu(self): - self._nu = None - - @property - def eta(self) -> Union[float, None]: - """Value of of eta angle.""" - if self.indegrees: - return degrees(self._eta) - else: - return self._eta - - @eta.setter - def eta(self, val): - if self.indegrees: - self._eta = radians(val) - else: - self._eta = val - - @eta.deleter - def eta(self): - self._eta = None - - @property - def chi(self) -> Union[float, None]: - """Value of of chi angle.""" - if self.indegrees: - return degrees(self._chi) - else: - return self._chi - - @chi.setter - def chi(self, val): - if self.indegrees: - self._chi = radians(val) - else: - self._chi = val - - @chi.deleter - def chi(self): - self._chi = None - - @property - def phi(self) -> Union[float, None]: - """Value of of phi angle.""" - if self.indegrees: - return degrees(self._phi) - else: - return self._phi - - @phi.setter - def phi(self, val): - if self.indegrees: - self._phi = radians(val) - else: - self._phi = val - - @phi.deleter - def phi(self): - self._phi = None + if pos.indegrees: + return cls( + **{key: radians(value) for key, value in pos.angles.items()}, + indegrees=False + ) + return pos @property - def asdict(self) -> Dict[str, float]: + def asdict(self): """Return dictionary of diffractometer angles. Returns @@ -228,10 +110,12 @@ def asdict(self) -> Dict[str, float]: Dict[str, float] Dictionary of axis names and angle values. """ - return {field: getattr(self, field) for field in self.fields} + class_info = self.angles.copy() + class_info["indegrees"] = self.indegrees + return class_info @property - def astuple(self) -> Tuple[float, float, float, float, float, float]: + def astuple(self) -> Tuple[Any, ...]: """Return tuple of diffractometer angles. Returns @@ -239,10 +123,7 @@ def astuple(self) -> Tuple[float, float, float, float, float, float]: Tuple[float, float, float, float, float, float] Tuple of angle values. """ - mu, delta, nu, eta, chi, phi = tuple( - getattr(self, field) for field in self.fields - ) - return mu, delta, nu, eta, chi, phi + return tuple(self.angles.values()) def get_rotation_matrices( @@ -388,8 +269,6 @@ def get_q_phi(pos: Position) -> np.ndarray: """ pos_in_rad = Position.asradians(pos) [MU, DELTA, NU, ETA, CHI, PHI] = get_rotation_matrices(pos_in_rad) - # Equation 12: Compute the momentum transfer vector in the lab frame y = np.array([[0], [1], [0]]) q_lab = (NU @ DELTA - I) @ y - # Transform this into the phi frame. - return inv(PHI) @ inv(CHI) @ inv(ETA) @ inv(MU) @ q_lab + return np.array(inv(PHI) @ inv(CHI) @ inv(ETA) @ inv(MU) @ q_lab) diff --git a/src/diffcalc/ub/calc.py b/src/diffcalc/ub/calc.py index fbed45b..950dec3 100644 --- a/src/diffcalc/ub/calc.py +++ b/src/diffcalc/ub/calc.py @@ -10,17 +10,17 @@ from copy import deepcopy from itertools import product from math import acos, asin, cos, degrees, pi, radians, sin -from typing import List, Optional, Sequence, Tuple, Union +from typing import Any, Dict, List, Optional, Sequence, Tuple, Union import numpy as np from diffcalc.hkl.geometry import Position, get_q_phi, get_rotation_matrices from diffcalc.ub.crystal import Crystal from diffcalc.ub.fitting import fit_crystal, fit_u_matrix from diffcalc.ub.reference import OrientationList, Reflection, ReflectionList +from diffcalc.ub.systems import available_systems from diffcalc.util import ( SMALL, DiffcalcException, - allnum, bound, cross3, dot3, @@ -99,7 +99,7 @@ def get_array(self, UB: Optional[np.ndarray] = None) -> np.ndarray: n_ref_new = UB @ n_ref_array else: n_ref_new = inv(UB) @ n_ref_array - return n_ref_new / norm(n_ref_new) + return np.array(n_ref_new / norm(n_ref_new)) def set_array(self, n_ref: np.ndarray) -> None: """Set reference vector coordinates from NumPy array. @@ -130,6 +130,14 @@ def set_array(self, n_ref: np.ndarray) -> None: (r1, r2, r3) = tuple(n_ref.T[0].tolist()) self.n_ref = (r1, r2, r3) + @property + def asdict(self) -> Dict[str, Any]: + return self.__dict__ + + # @classmethod + # def fromdict(cls, data: Dict[str, Any]) -> "ReferenceVector": + # return cls(**data) + class UBCalculation: """Class containing information required for for UB matrix calculation. @@ -372,15 +380,14 @@ def __str_lines_orient(self) -> List[str]: def set_lattice( self, name: str, - system: Optional[ - Union[str, float] - ] = None, # FIXME: Cannot set Union type for positional arguments + system: Optional[str] = None, a: Optional[float] = None, b: Optional[float] = None, c: Optional[float] = None, alpha: Optional[float] = None, beta: Optional[float] = None, gamma: Optional[float] = None, + indegrees: bool = True, ) -> None: """Set crystal lattice parameters using shortform notation. @@ -400,15 +407,14 @@ def set_lattice( (a,) -- assumes Cubic system (a, c) -- assumes Tetragonal system (a, b, c) -- assumes Orthorombic system - (a, b, c, angle) -- assumes Monoclinic system with beta not equal to 90 or - Hexagonal system if a = b and gamma = 120 + (a, b, c, beta) -- assumes Monoclinic system (a, b, c, alpha, beta, gamma) -- sets Triclinic system Parameters ---------- name: str Crystal name - system: Optional[float], default = None + system: Optional[str], default = None Crystal lattice type. a: Optional[float], default = None Crystal lattice parameter. @@ -417,49 +423,30 @@ def set_lattice( c: Optional[float], default = None Crystal lattice parameter. alpha: Optional[float], default = None - Crystal lattice angle. + Crystal lattice angle in degrees beta: Optional[float], default = None - Crystal lattice angle. + Crystal lattice angle in degrees gamma: Optional[float], default = None - Crystal lattice angle. + Crystal lattice angle in degrees """ - if not isinstance(name, str): - raise TypeError("Invalid crystal name.") - shortform = tuple( - val for val in (system, a, b, c, alpha, beta, gamma) if val is not None - ) - if not shortform: - raise TypeError("Please specify unit cell parameters.") - elif allnum(shortform): - sf = shortform - if len(sf) == 1: - system = "Cubic" - elif len(sf) == 2: - system = "Tetragonal" - elif len(sf) == 3: - system = "Orthorhombic" - elif len(sf) == 4: - if is_small(float(sf[0]) - float(sf[1])) and sf[3] == 120: - system = "Hexagonal" - else: - system = "Monoclinic" - elif len(sf) == 6: - system = "Triclinic" - else: - raise TypeError( - "Invalid number of input parameters to set unit lattice." - ) - fullform = (system,) + shortform - else: - if not isinstance(shortform[0], str): - raise TypeError("Invalid unit cell parameters specified.") - fullform = shortform - if self.name is None: + assumed_systems = { + 1: "Cubic", + 2: "Tetragonal", + 3: "Orthorombic", + 4: "Monoclinic", + 6: "Triclinic", + } + + params = [val for val in (a, b, c, alpha, beta, gamma) if val is not None] + + if not system: + system = assumed_systems[len(params)] + elif system not in available_systems: raise DiffcalcException( - "Cannot set lattice until a UBCalcaluation has been started " - "with newubcalc" + f"invalid system, choose from one of: {available_systems}" ) - self.crystal = Crystal(name, *fullform) + + self.crystal = Crystal(name, system, params, indegrees=indegrees) ### Reference vector ### @property @@ -553,7 +540,10 @@ def add_reflection( identifying tag for the reflection """ if self.reflist is None: - raise DiffcalcException("No UBCalculation loaded") + raise DiffcalcException( + "No UBCalculation loaded" + ) # self.reflist is never None. + # i.e. upon initialising UBCalculation(), it's generated. self.reflist.add_reflection(hkl, position, energy, tag) def edit_reflection( @@ -1208,9 +1198,9 @@ def _fit_ub_uncon( lattice_name = self.crystal.get_lattice()[0] return new_umatrix, ( lattice_name, - ax, - bx, - cx, + float(ax), + float(bx), + float(cx), degrees(alpha), degrees(beta), degrees(gamma), @@ -1231,7 +1221,8 @@ def get_miscut(self) -> Tuple[float, np.ndarray]: rotation_angle = 0.0 else: rotation_axis = rotation_axis / norm(rotation_axis) - cos_rotation_angle = bound(dot3(self.surf_nphi, surf_rot) / norm(surf_rot)) + vector_product = dot3(self.surf_nphi, surf_rot) + cos_rotation_angle = bound(vector_product / float(norm(surf_rot))) rotation_angle = acos(cos_rotation_angle) return rotation_angle, rotation_axis @@ -1263,7 +1254,9 @@ def get_miscut_from_hkl( return None, None axis = axis / norm(axis) try: - miscut = acos(bound(dot3(q_vec, hkl_nphi) / (norm(q_vec) * norm(hkl_nphi)))) + miscut = acos( + bound(dot3(q_vec, hkl_nphi) / float(norm(q_vec) * norm(hkl_nphi))) + ) except AssertionError: return 0, (0, 0, 0) return degrees(miscut), (axis[0, 0], axis[1, 0], axis[2, 0]) @@ -1355,7 +1348,7 @@ def _rescale_unit_cell( Scaling factor and updated crystal lattice parameters. """ q_vec = get_q_phi(pos) - q_hkl = norm(q_vec) / wavelength + q_hkl = float(norm(q_vec) / wavelength) d_hkl = self.crystal.get_hkl_plane_distance(hkl) sc = 1 / (q_hkl * d_hkl) name, a1, a2, a3, alpha1, alpha2, alpha3 = self.crystal.get_lattice() @@ -1375,3 +1368,33 @@ def _rescale_unit_cell( alpha2, alpha3, ) + + @property + def asdict(self) -> Dict[str, Any]: + return { + "name": self.name, + "crystal": self.crystal.asdict if self.crystal is not None else None, + "reflist": self.reflist.asdict, + "orientlist": self.orientlist.asdict, + "reference": self.reference.asdict, + "surface": self.surface.asdict, + "u_matrix": self.U.tolist() if self.U is not None else None, + "ub_matrix": self.UB.tolist() if self.UB is not None else None, + } + + @classmethod + def fromdict(cls, data: Dict[str, Any]) -> "UBCalculation": + # need to return exactly the same object. + ubcalc = cls(data["name"]) + ubcalc.crystal = ( + Crystal.fromdict(data["crystal"]) if data["crystal"] is not None else None + ) + ubcalc.reflist = ReflectionList.fromdict(data["reflist"]) + ubcalc.orientlist = OrientationList.fromdict(data["orientlist"]) + ubcalc.reference = ReferenceVector(**data["reference"]) + ubcalc.surface = ReferenceVector(**data["surface"]) + ubcalc.U = np.array(data["u_matrix"]) if data["u_matrix"] is not None else None + ubcalc.UB = ( + np.array(data["ub_matrix"]) if data["ub_matrix"] is not None else None + ) + return ubcalc diff --git a/src/diffcalc/ub/crystal.py b/src/diffcalc/ub/crystal.py index 17cd4f0..05368bc 100644 --- a/src/diffcalc/ub/crystal.py +++ b/src/diffcalc/ub/crystal.py @@ -4,13 +4,20 @@ crystal plane geometric properties. """ from math import acos, cos, degrees, pi, radians, sin, sqrt -from typing import List, Optional, Tuple +from typing import Any, Dict, List, Tuple, Union import numpy as np -from diffcalc.util import allnum, angle_between_vectors, zero_round +from diffcalc.ub.systems import Systems, SystemType, available_systems +from diffcalc.util import DiffcalcException, angle_between_vectors, zero_round from numpy.linalg import inv +def lists_equal(list1: List[Any], list2: List[Any]) -> bool: + if len(list1) != len(list2): + return False + return bool(np.all([item in list2 for item in list1])) + + class Crystal: """Class containing crystal lattice information and auxiliary routines. @@ -40,16 +47,21 @@ class Crystal: B matrix. """ + mapping = { + "a": "a1", + "b": "a2", + "c": "a3", + "alpha": "alpha1", + "beta": "alpha2", + "gamma": "alpha3", + } + def __init__( self, name: str, - system: Optional[str] = None, - a: Optional[float] = None, - b: Optional[float] = None, - c: Optional[float] = None, - alpha: Optional[float] = None, - beta: Optional[float] = None, - gamma: Optional[float] = None, + system: str, + lattice_params: Union[Dict[str, float], List[float]], + indegrees: bool = True, ) -> None: """Create a new crystal lattice and calculates B matrix. @@ -73,32 +85,34 @@ def __init__( Crystal lattice angle. """ self.name = name - args = tuple( - val for val in (system, a, b, c, alpha, beta, gamma) if val is not None - ) - if allnum(args): - if len(args) != 6: - raise ValueError( - "Crystal definition requires six lattice " - "parameters or crystal system name." + self.indegrees = indegrees + self.default_params: SystemType = Systems[system].value.copy() + + self.a1, self.a2, self.a3 = 0.0, 0.0, 0.0 + self.alpha1, self.alpha2, self.alpha3 = 0.0, 0.0, 0.0 + + if system in available_systems: + + required_params = [ + key for key, item in self.default_params.items() if item is None + ] + if not isinstance(lattice_params, dict): + lattice_params = dict(zip(required_params, lattice_params)) + + self.lattice_params = lattice_params + + params_equal = lists_equal(required_params, list(lattice_params.keys())) + if not params_equal: + raise DiffcalcException( + f"incorrect parameters for {system} system. " + f"This requires: {required_params} as floating point values" ) - # Set the direct lattice parameters - self.system = "Triclinic" - self.a1, self.a2, self.a3 = tuple(float(val) for val in args[:3]) - self.alpha1, self.alpha2, self.alpha3 = tuple( - radians(float(val)) for val in args[3:] - ) - self._set_reciprocal_cell( - self.a1, self.a2, self.a3, self.alpha1, self.alpha2, self.alpha3 - ) + + self.system = system else: - if not isinstance(args[0], str): - raise ValueError(f"Invalid crystal system name {args[0]}.") - self.system = args[0] - if allnum(args[1:]): - self._set_cell_for_system(system, a, b, c, alpha, beta, gamma) - else: - raise ValueError("Crystal lattice parameters must be numeric type.") + raise DiffcalcException(f"system must be one of: {available_systems}") + + self._set_cell_for_system(lattice_params) def __str__(self) -> str: """Represent the crystal lattice information as a string. @@ -159,14 +173,10 @@ def _str_lines(self) -> List[str]: def _set_reciprocal_cell( self, - a1: float, - a2: float, - a3: float, - alpha1: float, - alpha2: float, - alpha3: float, ) -> None: - # Calculate the reciprocal lattice parameters + a1, a2, a3 = self.a1, self.a2, self.a3 + alpha1, alpha2, alpha3 = self.alpha1, self.alpha2, self.alpha3 + beta2 = acos( (cos(alpha1) * cos(alpha3) - cos(alpha2)) / (sin(alpha1) * sin(alpha3)) ) @@ -192,8 +202,6 @@ def _set_reciprocal_cell( b2 = 2 * pi * a1 * a3 * sin(alpha2) / volume b3 = 2 * pi * a1 * a2 * sin(alpha3) / volume - # Calculate the BMatrix from the direct and reciprical parameters. - # Reference: Busang and Levy (1967) self.B = np.array( [ [b1, b2 * cos(beta3), b3 * cos(beta2)], @@ -220,7 +228,7 @@ def get_lattice(self) -> Tuple[str, float, float, float, float, float, float]: degrees(self.alpha3), ) - def get_lattice_params(self) -> Tuple[str, Tuple[float, ...]]: + def get_lattice_params(self) -> Tuple[str, Dict[str, float]]: """Get crystal name and non-redundant set of crystal lattice parameters. Returns @@ -229,118 +237,26 @@ def get_lattice_params(self) -> Tuple[str, Tuple[float, ...]]: Crystal name and minimal set of parameters for the crystal lattice system. """ - try: - if self.system == "Triclinic": - return self.system, ( - self.a1, - self.a2, - self.a3, - degrees(self.alpha1), - degrees(self.alpha2), - degrees(self.alpha3), - ) - elif self.system == "Monoclinic": - return self.system, ( - self.a1, - self.a2, - self.a3, - degrees(self.alpha2), - ) - elif self.system == "Orthorhombic": - return self.system, (self.a1, self.a2, self.a3) - elif self.system == "Tetragonal" or self.system == "Hexagonal": - return self.system, (self.a1, self.a3) - elif self.system == "Rhombohedral": - return self.system, (self.a1, degrees(self.alpha1)) - elif self.system == "Cubic": - return self.system, (self.a1,) - else: - raise TypeError( - "Invalid crystal system parameter: %s" % str(self.system) - ) - except ValueError as e: - raise TypeError from e - - def _get_cell_for_system( - self, system: str - ) -> Tuple[float, float, float, float, float, float]: - if system == "Triclinic": - return ( - self.a1, - self.a2, - self.a3, - radians(self.alpha1), - radians(self.alpha2), - radians(self.alpha3), - ) - elif system == "Monoclinic": - return (self.a1, self.a2, self.a3, pi / 2, radians(self.alpha2), pi / 2) - elif system == "Orthorhombic": - return (self.a1, self.a2, self.a3, pi / 2, pi / 2, pi / 2) - elif system == "Tetragonal": - return (self.a1, self.a1, self.a3, pi / 2, pi / 2, pi / 2) - elif system == "Rhombohedral": - return ( - self.a1, - self.a1, - self.a1, - radians(self.alpha1), - radians(self.alpha1), - radians(self.alpha1), - ) - elif system == "Hexagonal": - return (self.a1, self.a1, self.a3, pi / 2, pi / 2, 2 * pi / 3) - elif system == "Cubic": - return (self.a1, self.a1, self.a1, pi / 2, pi / 2, pi / 2) - else: - raise TypeError("Invalid crystal system parameter: %s" % str(system)) + return self.system, self.lattice_params - def _set_cell_for_system( - self, - system: str, - a: float, - b: Optional[float] = None, - c: Optional[float] = None, - alpha: Optional[float] = None, - beta: Optional[float] = None, - gamma: Optional[float] = None, - ) -> None: - args = tuple(val for val in (a, b, c, alpha, beta, gamma) if val is not None) - try: - if len(args) == 6 or system == "Triclinic": - ( - self.a1, - self.a2, - self.a3, - self.alpha1, - self.alpha2, - self.alpha3, - ) = args - elif system == "Monoclinic": - (self.a1, self.a2, self.a3, self.alpha2) = args - elif system == "Orthorhombic": - (self.a1, self.a2, self.a3) = args - elif system == "Tetragonal" or system == "Hexagonal": - (self.a1, self.a3) = args - elif system == "Rhombohedral": - (self.a1, self.alpha1) = args - elif system == "Cubic": - (self.a1,) = args + def _set_cell_for_system(self, params: Dict[str, float]) -> None: + default_params = self.default_params + + for param_key, param_value in params.items(): + if (param_key == ("alpha" or "beta" or "gamma")) and self.indegrees: + default_params[param_key] = radians(param_value) else: - raise TypeError("Invalid crystal system parameter: %s" % str(system)) - except ValueError as e: - raise TypeError from e - ( - self.a1, - self.a2, - self.a3, - self.alpha1, - self.alpha2, - self.alpha3, - ) = self._get_cell_for_system(system) - self._set_reciprocal_cell( - self.a1, self.a2, self.a3, self.alpha1, self.alpha2, self.alpha3 - ) + default_params[param_key] = param_value + + for default_key, default_value in default_params.items(): + if isinstance(default_value, str): + default_params[default_key] = default_params[default_value] + + for key in default_params: + attr = self.mapping[key] + setattr(self, attr, default_params[key]) + + self._set_reciprocal_cell() def get_hkl_plane_distance(self, hkl: Tuple[float, float, float]) -> float: """Calculate distance between crystal lattice planes. @@ -377,9 +293,23 @@ def get_hkl_plane_angle( float The angle between the crystal lattice planes. """ - hkl1 = np.array([hkl1]).T - hkl2 = np.array([hkl2]).T - nphi1 = self.B @ hkl1 - nphi2 = self.B @ hkl2 + hkl1_transpose = np.array([hkl1]).T + hkl2_transpose = np.array([hkl2]).T + nphi1 = self.B @ hkl1_transpose + nphi2 = self.B @ hkl2_transpose angle = angle_between_vectors(nphi1, nphi2) return angle + + @property + def asdict(self) -> Dict[str, Any]: + """Serialise the crystal into a JSON compatible dictionary""" + return { + "name": self.name, + "system": self.system, + "lattice_params": self.lattice_params, + "indegrees": self.indegrees, + } + + @classmethod + def fromdict(cls, data: Dict[str, Any]) -> "Crystal": + return Crystal(**data) diff --git a/src/diffcalc/ub/fitting.py b/src/diffcalc/ub/fitting.py index 19b8d6a..dd938ac 100644 --- a/src/diffcalc/ub/fitting.py +++ b/src/diffcalc/ub/fitting.py @@ -4,7 +4,7 @@ and U matrix using reflection data. """ from math import atan2, cos, pi, sin, sqrt -from typing import List, Sequence, Tuple +from typing import List, Tuple import numpy as np from diffcalc.hkl.geometry import Position, get_rotation_matrices @@ -27,25 +27,28 @@ def _get_refl_hkl( def _func_crystal( - vals: Sequence[float], uc_system: str, refl_data: Tuple[np.ndarray, Position, float] + vals: List[float], + uc_system: str, + refl_data: List[Tuple[np.ndarray, Position, float]], ) -> float: try: - trial_cr = Crystal("trial", uc_system, *vals) + + trial_cr = Crystal(name="trial", system=uc_system, lattice_params=vals) except Exception: return 1e6 - res = 0 + res: float = 0.0 for (hkl_vals, pos_vals, en) in refl_data: wl = 12.3984 / en [_, DELTA, NU, _, _, _] = get_rotation_matrices(pos_vals) q_pos = (NU @ DELTA - I) @ np.array([[0], [2 * pi / wl], [0]]) q_hkl = trial_cr.B @ hkl_vals - res += (norm(q_pos) - norm(q_hkl)) ** 2 + res += float((norm(q_pos) - norm(q_hkl)) ** 2) return res def _func_orient( - vals, crystal: Crystal, refl_data: Tuple[np.ndarray, Position, float] + vals, crystal: Crystal, refl_data: List[Tuple[np.ndarray, Position, float]] ) -> float: quat = _get_quat_from_u123(*vals) trial_u = _get_rot_matrix(*quat) @@ -67,19 +70,19 @@ def _get_rot_matrix(q0: float, q1: float, q2: float, q3: float) -> np.ndarray: rot = np.array( [ [ - q0 ** 2 + q1 ** 2 - q2 ** 2 - q3 ** 2, + q0**2 + q1**2 - q2**2 - q3**2, 2.0 * (q1 * q2 - q0 * q3), 2.0 * (q1 * q3 + q0 * q2), ], [ 2.0 * (q1 * q2 + q0 * q3), - q0 ** 2 - q1 ** 2 + q2 ** 2 - q3 ** 2, + q0**2 - q1**2 + q2**2 - q3**2, 2.0 * (q2 * q3 - q0 * q1), ], [ 2.0 * (q1 * q3 - q0 * q2), 2.0 * (q2 * q3 + q0 * q1), - q0 ** 2 - q1 ** 2 - q2 ** 2 + q3 ** 2, + q0**2 - q1**2 - q2**2 + q3**2, ], ] ) @@ -166,7 +169,7 @@ def fit_crystal(crystal: Crystal, refl_list: List[Reflection]) -> Crystal: """ try: xtal_system, xtal_params = crystal.get_lattice_params() - start = xtal_params + start = list(xtal_params.values()) lower = [ 0, ] * len(xtal_params) @@ -189,7 +192,7 @@ def fit_crystal(crystal: Crystal, refl_list: List[Reflection]) -> Crystal: ) vals = res.x - res_cr = Crystal("trial", xtal_system, *vals) + res_cr = Crystal(name="trial", system=xtal_system, lattice_params=vals) # res_cr._set_cell_for_system(uc_system, *vals) return res_cr @@ -248,3 +251,12 @@ def fit_u_matrix( # zr = q3 / sqrt(1. - q0 * q0) # print angle * TODEG, (xr, yr, zr), res return res_u + + +# crystal = Crystal( +# name="test", system="Tetragonal", lattice_params={"a": 4.913, "c": 5.405} +# ) +# list_of_reflections = [ +# Reflection(0.0, 0.0, 1.0, Position(7.31, 0, 10.62, 0, 0, 0), 12.39842, "refl1") +# ] +# new_crystal = fit_crystal(crystal, list_of_reflections) diff --git a/src/diffcalc/ub/reference.py b/src/diffcalc/ub/reference.py index 9d7ff00..dbe36de 100644 --- a/src/diffcalc/ub/reference.py +++ b/src/diffcalc/ub/reference.py @@ -1,12 +1,29 @@ """Module providing objects for working with reference reflections and orientations.""" import dataclasses -from typing import List, Tuple, Union +from typing import Any, Dict, List, Tuple, Union from diffcalc.hkl.geometry import Position @dataclasses.dataclass -class Reflection: +class Reference: + h: float + k: float + l: float + pos: Position + tag: str + + @property + def asdict(self) -> Dict[str, Any]: + return {} + + @property + def astuple(self) -> Tuple[Any, ...]: + return () + + +@dataclasses.dataclass +class Reflection(Reference): """Class containing reference reflection information. Attributes @@ -25,59 +42,139 @@ class Reflection: Identifying tag for the reflection. """ + energy: float + + @property + def astuple( + self, + ) -> Tuple[Tuple[float, float, float], Tuple[Any, ...], float, str,]: + """Return reference reflection data as tuple. + + Returns + ------- + Tuple[Tuple[float, float, float], + Tuple[Any, ...], + float, + str] + Tuple containing miller indices, position object, energy and + reflection tag. + """ + h, k, l, pos, tag, en = dataclasses.astuple(self) + return (h, k, l), pos, tag, en + + @property + def asdict(self) -> Dict[str, Any]: + """Return reference reflection data as dictionary. + + Returns + ------- + JSONReflection + Class structure containing miller indices, position as a dictionary, energy + and reflection tag. + """ + class_info = self.__dict__.copy() + class_info["pos"] = self.pos.asdict + return class_info + + @classmethod + def fromdict(cls, data: Dict[str, Any]) -> Reference: + """Create reflection object from a dictionary. + + Parameters + ---------- + data : JSONReflection + Class structure containing miller indices, position as a dictionary, energy + and reflection tag + + Returns + ------- + Reflection + An instance of the Reflection class. + """ + return cls( + data["h"], + data["k"], + data["l"], + Position(**data["pos"]), + data["tag"], + data["energy"], + ) + + +@dataclasses.dataclass +class Orientation(Reference): + """Class containing reference orientation information. + + Attributes + ---------- h: float + h miller index. k: float + k miller index. l: float + l miller index. + x: float + x coordinate in laboratory system. + y: float + y coordinate in laboratory system. + z: float + z coordinate in laboratory system. pos: Position - energy: float + Diffractometer position object. tag: str + Identifying tag for the orientation. + """ - def __post_init__(self): - """Check input argument types. - - Raises - ------ - TypeError - If pos argument has invalid type. - """ - if not isinstance(self.pos, Position): - raise TypeError(f"Invalid position object type {type(self.pos)}.") + x: float + y: float + z: float @property def astuple( self, ) -> Tuple[ + Tuple[float, float, float], Tuple[float, float, float], Tuple[float, float, float, float, float, float], - float, str, ]: - """Return reference reflection data as tuple. + """Return reference orientation data as tuple. Returns ------- Tuple[Tuple[float, float, float], + Tuple[float, float, float], Tuple[float, float, float, float, float, float], - float, str] - Tuple containing miller indices, position object, energy and - reflection tag. + Tuple containing miller indices, laboratory frame coordinates, + position object and orientation tag. """ - h, k, l, pos, en, tag = dataclasses.astuple(self) - return (h, k, l), pos.astuple, en, tag + h, k, l, pos, tag, x, y, z = dataclasses.astuple(self) + return (h, k, l), (x, y, z), pos, tag + @property + def asdict(self) -> Dict[str, Any]: + class_info = self.__dict__.copy() + class_info["pos"] = self.pos.asdict + return class_info + + @classmethod + def fromdict(cls, data: Dict[str, Any]) -> Reference: + return cls( + data["h"], + data["k"], + data["l"], + Position(**data["pos"]), + data["tag"], + data["x"], + data["y"], + data["z"], + ) -class ReflectionList: - """Class containing collection of reference reflections. - Attributes - ---------- - reflections: List[Reflection] - List containing reference reflections. - """ - - def __init__(self, reflections=None): - self.reflections: List[Reflection] = reflections if reflections else [] +@dataclasses.dataclass +class RefOrientList: + items: List[Reference] = dataclasses.field(default_factory=list) def get_tag_index(self, tag: str) -> int: """Get a reference reflection index. @@ -99,80 +196,19 @@ def get_tag_index(self, tag: str) -> int: ValueError If tag not found in reflection list. """ - _tag_list = [ref.tag for ref in self.reflections] + _tag_list = [ref.tag for ref in self.items] num = _tag_list.index(tag) return num - def add_reflection( - self, hkl: Tuple[float, float, float], pos: Position, energy: float, tag: str - ) -> None: - """Add a reference reflection. - - Adds a reference reflection object to the reflection list. - - Parameters - ---------- - hkl : Tuple[float, float, float] - Miller indices of the reflection - pos: Position - Object representing diffractometer angles - energy : float - Energy of the x-ray beam. - tag : str - Identifying tag for the reflection. - """ - self.reflections += [Reflection(*hkl, pos, energy, tag)] - - def edit_reflection( - self, - idx: Union[str, int], - hkl: Tuple[float, float, float], - pos: Position, - energy: float, - tag: str, - ) -> None: - """Change a reference reflection. - - Changes the reference reflection object in the reflection list. - - Parameters - ---------- - idx : Union[str, int] - Index or tag of the reflection to be changed - hkl : Tuple[float,float,float] - Miller indices of the reflection - position: Position - Object representing diffractometer angles. - energy : float - Energy of the x-ray beam. - tag : str - Identifying tag for the reflection. - - Raises - ------ - ValueError - Reflection with specified tag not found. - IndexError - Reflection with specified index not found. - """ - if isinstance(idx, str): - num = self.get_tag_index(idx) - else: - num = idx - 1 - if isinstance(pos, Position): - self.reflections[num] = Reflection(*hkl, pos, energy, tag) - else: - raise TypeError("Invalid position parameter type") - - def get_reflection(self, idx: Union[str, int]) -> Reflection: - """Get a reference reflection. + def get_item(self, idx: Union[str, int]) -> Reference: + """Get item from list of reference reflections/orientations. Get aon object representing reference reflection. Parameters ---------- idx : Union[str, int] - Index or tag of the reflection. + Index or tag of the reflection. Index same as python arrays; starts at 0 Returns ------- @@ -182,18 +218,18 @@ def get_reflection(self, idx: Union[str, int]) -> Reflection: Raises ------ ValueError - Reflection with the requested index/tan not present. + Reflection with the requested index/tag not present. IndexError Reflection with specified index not found. """ if isinstance(idx, str): num = self.get_tag_index(idx) else: - num = idx - 1 - return self.reflections[num] + num = idx + return self.items[num] - def remove_reflection(self, idx: Union[str, int]) -> None: - """Delete a reference reflection. + def remove_item(self, idx: Union[str, int]) -> None: + """Remove item from list of reference reflections/orientations. Parameters ---------- @@ -211,10 +247,10 @@ def remove_reflection(self, idx: Union[str, int]) -> None: num = self.get_tag_index(idx) else: num = idx - 1 - del self.reflections[num] + del self.items[num] - def swap_reflections(self, idx1: Union[str, int], idx2: Union[str, int]) -> None: - """Swap indices of two reference reflections. + def swap_items(self, idx1: Union[str, int], idx2: Union[str, int]) -> None: + """Swap indices of two items from list of reference reflections/orientations. Parameters ---------- @@ -238,9 +274,13 @@ def swap_reflections(self, idx1: Union[str, int], idx2: Union[str, int]) -> None num2 = self.get_tag_index(idx2) else: num2 = idx2 - 1 - orig1 = self.reflections[num1] - self.reflections[num1] = self.reflections[num2] - self.reflections[num2] = orig1 + orig1 = self.items[num1] + self.items[num1] = self.items[num2] + self.items[num2] = orig1 + + @property + def asdict(self) -> List[Dict[str, Any]]: + return [ref.asdict for ref in self.items] def __len__(self) -> int: """Return number of reference reflections in the list. @@ -250,7 +290,7 @@ def __len__(self) -> int: int Number of reference reflections. """ - return len(self.reflections) + return len(self.items) def __str__(self) -> str: """Represent the reference reflection list as a string. @@ -263,103 +303,117 @@ def __str__(self) -> str: return "\n".join(self._str_lines()) def _str_lines(self) -> List[str]: - """Table with reference reflection data. + return [] - Returns - ------- - List[str] - List containing reference reflection table rows. - """ - axes = tuple(fd.upper() for fd in Position.fields) - if not self.reflections: - return [" <<< none specified >>>"] - lines = [] +class ReflectionList(RefOrientList): + """Class containing collection of reference reflections. - fmt = " %6s %5s %5s %5s " + "%8s " * len(axes) + " TAG" - header_values = ("ENERGY", "H", "K", "L") + axes - lines.append(fmt % header_values) + Attributes + ---------- + reflections: List[Reflection] + List containing reference reflections. + """ - for n in range(1, len(self.reflections) + 1): - ref_tuple = self.get_reflection(n) - (h, k, l), pos, energy, tag = ref_tuple.astuple - if tag is None: - tag = "" - fmt = " %2d %6.3f % 4.2f % 4.2f % 4.2f " + "% 8.4f " * len(axes) + " %s" - values = (n, energy, h, k, l) + pos + (tag,) - lines.append(fmt % values) - return lines + def add_item( + self, hkl: Tuple[float, float, float], pos: Position, energy: float, tag: str + ) -> None: + """Add a reference reflection. + Adds a reference reflection object to the reflection list. -@dataclasses.dataclass -class Orientation: - """Class containing reference orientation information. + Parameters + ---------- + hkl : Tuple[float, float, float] + Miller indices of the reflection + pos: Position + Object representing diffractometer angles + energy : float + Energy of the x-ray beam. + tag : str + Identifying tag for the reflection. + """ + self.items += [Reflection(*hkl, pos, tag, energy)] - Attributes - ---------- - h: float - h miller index. - k: float - k miller index. - l: float - l miller index. - x: float - x coordinate in laboratory system. - y: float - y coordinate in laboratory system. - z: float - z coordinate in laboratory system. - pos: Position - Diffractometer position object. - tag: str - Identifying tag for the orientation. - """ + def edit_item( + self, + idx: Union[str, int], + hkl: Tuple[float, float, float], + pos: Position, + energy: float, + tag: str, + ) -> None: + """Change a reference reflection. - h: float - k: float - l: float - x: float - y: float - z: float - pos: Position - tag: str + Changes the reference reflection object in the reflection list. - def __post_init__(self): - """Check input argument types. + Parameters + ---------- + idx : Union[str, int] + Index or tag of the reflection to be changed + hkl : Tuple[float,float,float] + Miller indices of the reflection + position: Position + Object representing diffractometer angles. + energy : float + Energy of the x-ray beam. + tag : str + Identifying tag for the reflection. Raises ------ - TypeError - If pos argument has invalid type. + ValueError + Reflection with specified tag not found. + IndexError + Reflection with specified index not found. """ - if not isinstance(self.pos, Position): - raise TypeError(f"Invalid position object type {type(self.pos)}.") + if isinstance(idx, str): + num = self.get_tag_index(idx) + else: + num = idx - 1 - @property - def astuple( - self, - ) -> Tuple[ - Tuple[float, float, float], - Tuple[float, float, float], - Tuple[float, float, float, float, float, float], - str, - ]: - """Return reference orientation data as tuple. + self.items[num] = Reflection(*hkl, pos, tag, energy) + + @classmethod + def fromdict(cls, data: List[Dict[str, Any]]) -> "ReflectionList": + reflections = [Reflection.fromdict(each_ref) for each_ref in data] + return cls(reflections) + + def _str_lines(self) -> List[str]: + """Table with reference reflection data. Returns ------- - Tuple[Tuple[float, float, float], - Tuple[float, float, float], - Tuple[float, float, float, float, float, float], - str] - Tuple containing miller indices, laboratory frame coordinates, - position object and orientation tag. + List[str] + List containing reference reflection table rows. """ - h, k, l, x, y, z, pos, tag = dataclasses.astuple(self) - return (h, k, l), (x, y, z), pos.astuple, tag + axes = tuple(fd.name.upper() for fd in dataclasses.fields(Position)) + if not self.items: + return [" <<< none specified >>>"] + + lines = [] + + fmt = " %6s %5s %5s %5s " + "%8s " * (len(axes) - 1) + " %4s" + " %4s" + header_values = ("ENERGY", "H", "K", "L") + axes + ("TAG",) + lines.append(fmt % header_values) + + for n in range(len(self.items)): + ref_tuple = self.get_item(n) + (h, k, l), pos, tag, energy = ref_tuple.astuple + if tag is None: + tag = "" + fmt = ( + " %2d %6.3f % 4.2f % 4.2f % 4.2f " + + "% 8.4f " * (len(axes) - 1) + + " %4r" + + " %9s" + ) + values = (n, energy, h, k, l) + pos + (tag,) + lines.append(fmt % values) + return lines -class OrientationList: +class OrientationList(RefOrientList): """Class containing collection of reference orientations. Attributes @@ -368,34 +422,7 @@ class OrientationList: List containing reference orientations. """ - def __init__(self, orientations=None): - self.orientations: List[Orientation] = orientations if orientations else [] - - def get_tag_index(self, tag: str) -> int: - """Get a reference orientation index. - - Get a reference orientation index for the provided orientation tag. - - Parameters - ---------- - tag : str - identifying tag for the orientation - - Returns - ------- - int: - The reference orientation index. - - Raises - ------ - ValueError - If tag not found in orientations list. - """ - _tag_list = [orient.tag for orient in self.orientations] - num = _tag_list.index(tag) - return num - - def add_orientation( + def add_item( self, hkl: Tuple[float, float, float], xyz: Tuple[float, float, float], @@ -419,11 +446,11 @@ def add_orientation( identifying tag for the orientation. """ if isinstance(pos, Position): - self.orientations += [Orientation(*hkl, *xyz, pos, tag)] + self.items += [Orientation(*hkl, pos, tag, *xyz)] else: raise TypeError("Invalid position parameter type") - def edit_orientation( + def edit_item( self, idx: Union[str, int], hkl: Tuple[float, float, float], @@ -460,108 +487,13 @@ def edit_orientation( num = self.get_tag_index(idx) else: num = idx - 1 - if isinstance(pos, Position): - self.orientations[num] = Orientation(*hkl, *xyz, pos, tag) - else: - raise TypeError(f"Invalid position parameter type {type(pos)}") - - def get_orientation(self, idx: Union[str, int]) -> Orientation: - """Get a reference orientation. - - Get an object representing reference orientation. - - Parameters - ---------- - idx : Union[str, int] - Index or tag of the orientation. - - Returns - ------- - Orientation - Object representing reference orientation. - - Raises - ------ - ValueError - Orientation with the requested index/tag not present. - IndexError - Orientation with specified index not found. - """ - if isinstance(idx, str): - num = self.get_tag_index(idx) - else: - num = idx - 1 - return self.orientations[num] - def remove_orientation(self, idx: Union[str, int]) -> None: - """Delete a reference orientation. + self.items[num] = Orientation(*hkl, pos, tag, *xyz) - Parameters - ---------- - idx : Union[str, int] - Index or tag of the deteled orientation. - - Raises - ------ - ValueError - Orientation with the requested index/tag not present. - IndexError - Orientation with specified index not found. - """ - if isinstance(idx, str): - num = self.get_tag_index(idx) - else: - num = idx - 1 - del self.orientations[num] - - def swap_orientations(self, idx1: Union[str, int], idx2: Union[str, int]) -> None: - """Swap indices of two reference orientations. - - Parameters - ---------- - idx1 : Union[str, int] - Index or tag of the first orientation to be swapped. - idx2 : Union[str, int] - Index or tag of the second orientation to be swapped. - - Raises - ------ - ValueError - Orientation with the requested index/tag not present. - IndexError - Orientation with specified index not found. - """ - if isinstance(idx1, str): - num1 = self.get_tag_index(idx1) - else: - num1 = idx1 - 1 - if isinstance(idx2, str): - num2 = self.get_tag_index(idx2) - else: - num2 = idx2 - 1 - orig1 = self.orientations[num1] - self.orientations[num1] = self.orientations[num2] - self.orientations[num2] = orig1 - - def __len__(self) -> int: - """Return number of reference orientations in the list. - - Returns - ------- - int - Number of reference orientationss. - """ - return len(self.orientations) - - def __str__(self) -> str: - """Represent the reference orientations list as a string. - - Returns - ------- - str - Table containing list of all orientations. - """ - return "\n".join(self._str_lines()) + @classmethod + def fromdict(cls, data: List[Dict[str, Any]]) -> "OrientationList": + orientations = [Orientation.fromdict(each_ref) for each_ref in data] + return cls(orientations) def _str_lines(self) -> List[str]: """Table with reference orientations data. @@ -571,25 +503,31 @@ def _str_lines(self) -> List[str]: List[str] List containing reference orientations table rows. """ - axes = tuple(fd.upper() for fd in Position.fields) - if not self.orientations: + axes = tuple(fd.name.upper() for fd in dataclasses.fields(Position)) + if not self.items: return [" <<< none specified >>>"] lines = [] - str_format = " %5s %5s %5s %5s %5s %5s " + "%8s " * len(axes) + " TAG" - header_values = ("H", "K", "L", "X", "Y", "Z") + axes + str_format = ( + " %5s %5s %5s %5s %5s %5s" + + " %9s " * (len(axes) - 1) + + " %4s" + + " %4s" + ) + header_values = ("H", "K", "L", "X", "Y", "Z") + axes + ("TAG",) lines.append(str_format % header_values) - for n in range(1, len(self.orientations) + 1): - orient = self.get_orientation(n) + for n in range(len(self.items)): + orient = self.get_item(n) (h, k, l), (x, y, z), angles, tag = orient.astuple if tag is None: tag = "" str_format = ( - " %2d % 4.2f % 4.2f % 4.2f " - + "% 4.2f % 4.2f % 4.2f " - + "% 8.4f " * len(axes) + " %5d % 5.2f % 5.2f % 5.2f " + + " %5.2f % 5.2f % 5.2f " + + " %9.4f" * (len(axes) - 1) + + " %8r" + " %s" ) values = (n, h, k, l, x, y, z) + angles + (tag,) diff --git a/src/diffcalc/ub/systems.py b/src/diffcalc/ub/systems.py new file mode 100644 index 0000000..b25f14d --- /dev/null +++ b/src/diffcalc/ub/systems.py @@ -0,0 +1,67 @@ +from enum import Enum +from math import pi +from typing import Dict, Optional, Union + +SystemType = Dict[str, Optional[Union[float, str]]] + + +class Systems(Enum): + Triclinic: SystemType = { + "a": None, + "b": None, + "c": None, + "alpha": None, + "beta": None, + "gamma": None, + } + Monoclinic: SystemType = { + "a": None, + "b": None, + "c": None, + "alpha": pi / 2, + "beta": None, + "gamma": pi / 2, + } + Orthorhombic: SystemType = { + "a": None, + "b": None, + "c": None, + "alpha": pi / 2, + "beta": pi / 2, + "gamma": pi / 2, + } + Tetragonal: SystemType = { + "a": None, + "b": "a", + "c": None, + "alpha": pi / 2, + "beta": pi / 2, + "gamma": pi / 2, + } + Rhombohedral: SystemType = { + "a": None, + "b": "a", + "c": "a", + "alpha": None, + "beta": "alpha", + "gamma": "alpha", + } + Hexagonal: SystemType = { + "a": None, + "b": "a", + "c": None, + "alpha": pi / 2, + "beta": pi / 2, + "gamma": 2 * pi / 3, + } + Cubic: SystemType = { + "a": None, + "b": "a", + "c": "a", + "alpha": pi / 2, + "beta": pi / 2, + "gamma": pi / 2, + } + + +available_systems = [item for item in dir(Systems) if not item.startswith("_")] diff --git a/src/diffcalc/util.py b/src/diffcalc/util.py index 48bc885..0183c2a 100644 --- a/src/diffcalc/util.py +++ b/src/diffcalc/util.py @@ -1,6 +1,6 @@ """Collection of auxiliary mathematical methods.""" from math import acos, cos, isclose, sin -from typing import Any, Sequence, Tuple +from typing import Any, Tuple import numpy as np from numpy.linalg import norm @@ -74,8 +74,8 @@ def xyz_rotation(axis: Tuple[float, float, float], angle: float) -> np.ndarray: np.ndarray Rotation matrix. """ - rot = Rotation.from_rotvec(angle * np.array(axis) / norm(np.array(axis))) - return rot.as_matrix() + rot: Rotation = Rotation.from_rotvec(angle * np.array(axis) / norm(np.array(axis))) + return np.array(rot.as_matrix()) class DiffcalcException(Exception): @@ -225,22 +225,6 @@ def isnum(o: Any) -> bool: return isinstance(o, (int, float)) -def allnum(lst: Sequence[Any]) -> bool: - """Check if all object types in the input sequence are either int or float. - - Parameters - ---------- - o: Sequence[Any] - Input object sequence to be checked. - - Returns - ------- - bool - If all object types in th sequence are either int or float. - """ - return not [o for o in lst if not isnum(o)] - - def is_small(x, tolerance=SMALL) -> bool: """Check if input value is 0 within tolerance. @@ -298,7 +282,7 @@ def normalised(vector: np.ndarray) -> np.ndarray: ndarray Normalised vector. """ - return vector * (1.0 / norm(vector)) + return vector * (1.0 / float(norm(vector))) def zero_round(num): diff --git a/tests/diffcalc/scenarios.py b/tests/diffcalc/scenarios.py index afa8d7c..1dbf8bd 100644 --- a/tests/diffcalc/scenarios.py +++ b/tests/diffcalc/scenarios.py @@ -265,8 +265,10 @@ def sessions(P=VliegPos): session4 = SessionScenario() session4.name = "test_orth" + # session4.lattice = (7.51, 7.73, 7.00, 106.0, 113.5, 99.5) session4.lattice = (1.41421, 1.41421, 1.00000, 90, 90, 90) - session4.system = "Orthorhombic" + # session4.system = "Orthorhombic" + session4.system = "Triclinic" session4.bmatrix = ((4.44288, 0, 0), (0, 4.44288, 0), (0, 0, 6.28319)) session4.ref1 = Reflection( 0,