From 266d1a4fa0584a9958c6c8c731d8b9c79143b4f7 Mon Sep 17 00:00:00 2001 From: Rose Yemelyanova Date: Tue, 19 Jul 2022 16:37:11 +0000 Subject: [PATCH 1/3] modified files to pass mypy checks. Greatly modified Crystal object to simplify logic, work still needs to be done to simplify Hkl object to pass flake8 checks. --- .azure-pipelines/ci.yml | 5 + .devcontainer/Dockerfile | 21 +++ .devcontainer/devcontainer.json | 54 +++++++ src/diffcalc/hkl/calc.py | 19 ++- src/diffcalc/hkl/geometry.py | 4 +- src/diffcalc/ub/calc.py | 173 ++++++++++++++++------- src/diffcalc/ub/crystal.py | 240 +++++++++++--------------------- src/diffcalc/ub/fitting.py | 34 +++-- src/diffcalc/ub/systems.py | 67 +++++++++ src/diffcalc/util.py | 24 +--- tests/diffcalc/scenarios.py | 4 +- 11 files changed, 395 insertions(+), 250 deletions(-) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 src/diffcalc/ub/systems.py 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..3ee3257 100644 --- a/src/diffcalc/hkl/calc.py +++ b/src/diffcalc/hkl/calc.py @@ -9,6 +9,7 @@ from typing import 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,8 @@ def _verify_virtual_angles( "anglesToVirtualAngles of %f" % virtual_angles_readback[key] ) raise DiffcalcException(s) + + +hkl = HklCalculation(UBCalculation(), Constraints({"qaz": 0, "alpha": 0, "eta": 0})) + +result = hkl.get_position(0, 0, 1, 0.1) diff --git a/src/diffcalc/hkl/geometry.py b/src/diffcalc/hkl/geometry.py index 438ce7d..297e941 100644 --- a/src/diffcalc/hkl/geometry.py +++ b/src/diffcalc/hkl/geometry.py @@ -388,8 +388,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..9219923 100644 --- a/src/diffcalc/ub/calc.py +++ b/src/diffcalc/ub/calc.py @@ -17,10 +17,10 @@ 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. @@ -372,15 +372,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 +399,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 +415,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, params, system, indegrees=indegrees) ### Reference vector ### @property @@ -1208,9 +1187,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 +1210,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 +1243,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 +1337,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 +1357,90 @@ def _rescale_unit_cell( alpha2, alpha3, ) + + +# def VliegPos(alpha=None, delta=None, gamma=None, omega=None, chi=None, phi=None): +# """Convert six-circle Vlieg diffractometer angles into 4S+2D You geometry""" +# sin_alpha = sin(radians(alpha)) +# cos_alpha = cos(radians(alpha)) +# sin_delta = sin(radians(delta)) +# cos_delta = cos(radians(delta)) +# sin_gamma = sin(radians(gamma)) +# cos_gamma = cos(radians(gamma)) +# asin_delta = degrees(asin(sin_delta * cos_gamma)) # Eq.(83) +# vals_delta = [asin_delta, 180.0 - asin_delta] +# idx, _ = min( +# [(i, abs(delta - d)) for i, d in enumerate(vals_delta)], key=lambda x: x[1] +# ) +# pos_delta = vals_delta[idx] +# sgn = sign(cos(radians(pos_delta))) +# pos_nu = degrees( +# atan2( +# sgn * (cos_delta * cos_gamma * sin_alpha + cos_alpha * sin_gamma), +# sgn * (cos_delta * cos_gamma * cos_alpha - sin_alpha * sin_gamma), +# ) +# ) # Eq.(84) +# return Position(mu=alpha, delta=pos_delta, nu=pos_nu, eta=omega, chi=chi, phi=phi) + + +# ubcalc = UBCalculation() +# ubcalc.add_reflection( +# (0, 1, 2), +# VliegPos(0.0000, 23.7405, 0.0000, 11.8703, 46.3100, 43.1304), +# 12.3984, +# "ref1", +# ) +# ubcalc.add_reflection( +# (1, 0, 3), +# VliegPos(0.0000, 34.4282, 0.000, 17.2141, 46.4799, 12.7852), +# 12.3984, +# "ref2", +# ) +# ubcalc.add_reflection( +# (2, 2, 6), +# VliegPos(0.0000, 82.8618, 0.000, 41.4309, 41.5154, 26.9317), +# 12.3984, +# "ref3", +# ) +# ubcalc.add_reflection( +# (4, 1, 4), +# VliegPos(0.0000, 71.2763, 0.000, 35.6382, 29.5042, 14.5490), +# 12.3984, +# "ref4", +# ) +# ubcalc.add_reflection( +# (8, 3, 1), +# VliegPos(0.0000, 97.8850, 0.000, 48.9425, 5.6693, 16.7929), +# 12.3984, +# "ref5", +# ) +# ubcalc.add_reflection( +# (6, 4, 5), +# VliegPos(0.0000, 129.6412, 0.000, 64.8206, 24.1442, 24.6058), +# 12.3984, +# "ref6", +# ) +# ubcalc.add_reflection( +# (3, 5, 7), +# VliegPos(0.0000, 135.9159, 0.000, 67.9579, 34.3696, 35.1816), +# 12.3984, +# "ref7", +# ) + +# a, b, c, alpha, beta, gamma = 7.51, 7.73, 7.00, 106.0, 113.5, 99.5 + +# ubcalc.set_lattice("Dalyite", "Triclinic", 7.51, 7.73, 7.00, 106.0, 113.5, 99.5) +# ubcalc.calc_ub("ref1", "ref2") + +# init_latt = ( +# 1.06 * a, +# 1.07 * b, +# 0.94 * c, +# 1.05 * alpha, +# 1.06 * beta, +# 0.95 * gamma, +# ) +# ubcalc.set_lattice("Dalyite", "Triclinic", *init_latt) +# ubcalc.set_miscut((0.2, 0.8, 0.1), 3.0, True) + +# ubcalc.fit_ub(["ref1", "ref2", "ref3", "ref4", "ref5", "ref6", "ref7"], True, True) diff --git a/src/diffcalc/ub/crystal.py b/src/diffcalc/ub/crystal.py index 17cd4f0..5c8c8da 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, + lattice_params: Union[Dict[str, float], List[float]], + system: str, + 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,27 @@ 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 + + # loop through params, and replace default_params[param] with the value there. + 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 +294,18 @@ 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 + + +# test = Crystal( +# name="test", lattice_params={"a": 4.913, "c": 5.405}, system="Tetragonal" +# ) +# test.get_hkl_plane_angle((0, 0, 1), (0, 1, 3)) + +# output = test.get_lattice_params() +# print("yay") 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/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, From 538a30f7750f4182c986aa8c30a98756f1456314 Mon Sep 17 00:00:00 2001 From: Rose Yemelyanova Date: Thu, 21 Jul 2022 15:11:26 +0000 Subject: [PATCH 2/3] added asdict and fromdict methods onto classes to serialise the objects in hklcalculation --- src/diffcalc/hkl/calc.py | 37 ++++++- src/diffcalc/hkl/constraints.py | 7 +- src/diffcalc/hkl/geometry.py | 19 ++-- src/diffcalc/ub/calc.py | 173 ++++++++++++++++---------------- src/diffcalc/ub/crystal.py | 34 ++++++- src/diffcalc/ub/reference.py | 149 ++++++++++++++++++++++----- 6 files changed, 297 insertions(+), 122 deletions(-) diff --git a/src/diffcalc/hkl/calc.py b/src/diffcalc/hkl/calc.py index 3ee3257..5cb090d 100644 --- a/src/diffcalc/hkl/calc.py +++ b/src/diffcalc/hkl/calc.py @@ -6,7 +6,7 @@ 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 @@ -1788,7 +1788,38 @@ def _verify_virtual_angles( ) 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), + ) + + +# test = UBCalculation("test") +# test.set_lattice(name="test", a=4.913, c=5.405) +# test.add_reflection( +# hkl=(0, 0, 1), +# position=Position(7.31, 0, 10.62, 0, 0, 0), +# energy=12.39842, +# tag="refl1", +# ) +# test.add_orientation(hkl=(0, 1, 0), xyz=(0, 1, 0), tag="plane") +# test.n_hkl = (1.0, 0.0, 0.0) + +# test.calc_ub("refl1", "plane") + + +# hkl = HklCalculation(test, Constraints({"qaz": 0, "alpha": 0, "eta": 0})) -hkl = HklCalculation(UBCalculation(), Constraints({"qaz": 0, "alpha": 0, "eta": 0})) +# hkldict = hkl.asdict +# hkl2 = HklCalculation.fromdict(hkldict) +# result = hkl.get_position(0, 0, 1, 0.1) -result = hkl.get_position(0, 0, 1, 0.1) +# print("a") 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 297e941..fb1dbb0 100644 --- a/src/diffcalc/hkl/geometry.py +++ b/src/diffcalc/hkl/geometry.py @@ -9,7 +9,7 @@ J. Appl. Cryst. (1999). 32, 614-623. """ from math import degrees, radians -from typing import Dict, Tuple, Union +from typing import Any, Dict, Tuple, Union import numpy as np from diffcalc.util import I, x_rotation, y_rotation, z_rotation @@ -42,13 +42,14 @@ class Position: If True, arguments are angles in degrees. """ - fields: Tuple[str, str, str, str, str, str] = ( + fields: Tuple[str, str, str, str, str, str, str] = ( "mu", "delta", "nu", "eta", "chi", "phi", + "indegrees", ) def __init__( @@ -83,7 +84,7 @@ def asdegrees(cls, pos: "Position") -> "Position": Position New Position object with angles in degrees. """ - res = cls(**pos.asdict, indegrees=pos.indegrees) + res = cls(**pos.asdict) res.indegrees = True return res @@ -101,7 +102,7 @@ def asradians(cls, pos: "Position") -> "Position": Position New Position object with angles in radians. """ - res = cls(**pos.asdict, indegrees=pos.indegrees) + res = cls(**pos.asdict) res.indegrees = False return res @@ -220,7 +221,7 @@ def phi(self): self._phi = None @property - def asdict(self) -> Dict[str, float]: + def asdict(self) -> Dict[str, Any]: """Return dictionary of diffractometer angles. Returns @@ -230,8 +231,12 @@ def asdict(self) -> Dict[str, float]: """ return {field: getattr(self, field) for field in self.fields} + # @classmethod + # def fromdict(cls, data: Mapping[str, Any]) -> "Position": + # return cls(**data) + @property - def astuple(self) -> Tuple[float, float, float, float, float, float]: + def astuple(self) -> Tuple[Any, ...]: """Return tuple of diffractometer angles. Returns @@ -239,7 +244,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( + mu, delta, nu, eta, chi, phi, _ = tuple( getattr(self, field) for field in self.fields ) return mu, delta, nu, eta, chi, phi diff --git a/src/diffcalc/ub/calc.py b/src/diffcalc/ub/calc.py index 9219923..d3a08d4 100644 --- a/src/diffcalc/ub/calc.py +++ b/src/diffcalc/ub/calc.py @@ -10,13 +10,19 @@ 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.crystal import Crystal, JSONCrystal from diffcalc.ub.fitting import fit_crystal, fit_u_matrix -from diffcalc.ub.reference import OrientationList, Reflection, ReflectionList +from diffcalc.ub.reference import ( + JSONOrientation, + JSONReflection, + OrientationList, + Reflection, + ReflectionList, +) from diffcalc.ub.systems import available_systems from diffcalc.util import ( SMALL, @@ -31,6 +37,28 @@ from numpy.linalg import inv, norm +@dataclasses.dataclass +class JSONReferenceVector: + n_ref: Tuple[float, float, float] + rlv: bool + + @property + def asdict(self): + return self.__dict__ + + +@dataclasses.dataclass +class JSONUBCalculation: + name: str + crystal: Optional[JSONCrystal] + reflist: List[JSONReflection] + orientlist: List[JSONOrientation] + reference: Dict[str, Any] + surface: Dict[str, Any] + u_matrix: Optional[List[List[float]]] + ub_matrix: Optional[List[List[float]]] + + @dataclasses.dataclass class ReferenceVector: """Class representing reference vector information. @@ -130,6 +158,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. @@ -438,7 +474,7 @@ def set_lattice( f"invalid system, choose from one of: {available_systems}" ) - self.crystal = Crystal(name, params, system, indegrees=indegrees) + self.crystal = Crystal(name, system, params, indegrees=indegrees) ### Reference vector ### @property @@ -532,7 +568,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( @@ -1358,89 +1397,51 @@ def _rescale_unit_cell( 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, + } -# def VliegPos(alpha=None, delta=None, gamma=None, omega=None, chi=None, phi=None): -# """Convert six-circle Vlieg diffractometer angles into 4S+2D You geometry""" -# sin_alpha = sin(radians(alpha)) -# cos_alpha = cos(radians(alpha)) -# sin_delta = sin(radians(delta)) -# cos_delta = cos(radians(delta)) -# sin_gamma = sin(radians(gamma)) -# cos_gamma = cos(radians(gamma)) -# asin_delta = degrees(asin(sin_delta * cos_gamma)) # Eq.(83) -# vals_delta = [asin_delta, 180.0 - asin_delta] -# idx, _ = min( -# [(i, abs(delta - d)) for i, d in enumerate(vals_delta)], key=lambda x: x[1] -# ) -# pos_delta = vals_delta[idx] -# sgn = sign(cos(radians(pos_delta))) -# pos_nu = degrees( -# atan2( -# sgn * (cos_delta * cos_gamma * sin_alpha + cos_alpha * sin_gamma), -# sgn * (cos_delta * cos_gamma * cos_alpha - sin_alpha * sin_gamma), -# ) -# ) # Eq.(84) -# return Position(mu=alpha, delta=pos_delta, nu=pos_nu, eta=omega, chi=chi, phi=phi) - - -# ubcalc = UBCalculation() -# ubcalc.add_reflection( -# (0, 1, 2), -# VliegPos(0.0000, 23.7405, 0.0000, 11.8703, 46.3100, 43.1304), -# 12.3984, -# "ref1", -# ) -# ubcalc.add_reflection( -# (1, 0, 3), -# VliegPos(0.0000, 34.4282, 0.000, 17.2141, 46.4799, 12.7852), -# 12.3984, -# "ref2", -# ) -# ubcalc.add_reflection( -# (2, 2, 6), -# VliegPos(0.0000, 82.8618, 0.000, 41.4309, 41.5154, 26.9317), -# 12.3984, -# "ref3", -# ) -# ubcalc.add_reflection( -# (4, 1, 4), -# VliegPos(0.0000, 71.2763, 0.000, 35.6382, 29.5042, 14.5490), -# 12.3984, -# "ref4", -# ) -# ubcalc.add_reflection( -# (8, 3, 1), -# VliegPos(0.0000, 97.8850, 0.000, 48.9425, 5.6693, 16.7929), -# 12.3984, -# "ref5", -# ) -# ubcalc.add_reflection( -# (6, 4, 5), -# VliegPos(0.0000, 129.6412, 0.000, 64.8206, 24.1442, 24.6058), -# 12.3984, -# "ref6", -# ) -# ubcalc.add_reflection( -# (3, 5, 7), -# VliegPos(0.0000, 135.9159, 0.000, 67.9579, 34.3696, 35.1816), -# 12.3984, -# "ref7", -# ) - -# a, b, c, alpha, beta, gamma = 7.51, 7.73, 7.00, 106.0, 113.5, 99.5 + @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 -# ubcalc.set_lattice("Dalyite", "Triclinic", 7.51, 7.73, 7.00, 106.0, 113.5, 99.5) -# ubcalc.calc_ub("ref1", "ref2") -# init_latt = ( -# 1.06 * a, -# 1.07 * b, -# 0.94 * c, -# 1.05 * alpha, -# 1.06 * beta, -# 0.95 * gamma, +# test = UBCalculation("test") +# test.set_lattice(name="test", a=4.913, c=5.405) +# test.add_reflection( +# hkl=(0, 0, 1), +# position=Position(7.31, 0, 10.62, 0, 0, 0), +# energy=12.39842, +# tag="refl1", # ) -# ubcalc.set_lattice("Dalyite", "Triclinic", *init_latt) -# ubcalc.set_miscut((0.2, 0.8, 0.1), 3.0, True) +# test.add_orientation(hkl=(0, 1, 0), xyz=(0, 1, 0), tag="plane") +# test.n_hkl = (1.0, 0.0, 0.0) + +# # test.calc_ub() + +# tdict = test.asdict -# ubcalc.fit_ub(["ref1", "ref2", "ref3", "ref4", "ref5", "ref6", "ref7"], True, True) +# test2 = UBCalculation.fromdict(tdict) +# print("a") diff --git a/src/diffcalc/ub/crystal.py b/src/diffcalc/ub/crystal.py index 5c8c8da..766777c 100644 --- a/src/diffcalc/ub/crystal.py +++ b/src/diffcalc/ub/crystal.py @@ -3,6 +3,7 @@ A module defining crystal lattice class and auxiliary methods for calculating crystal plane geometric properties. """ +from dataclasses import dataclass from math import acos, cos, degrees, pi, radians, sin, sqrt from typing import Any, Dict, List, Tuple, Union @@ -18,6 +19,17 @@ def lists_equal(list1: List[Any], list2: List[Any]) -> bool: return bool(np.all([item in list2 for item in list1])) +@dataclass +class JSONCrystal: + name: str + system: str + lattice_params: Dict[str, float] + + @property + def asdict(self): + return self.__dict__ + + class Crystal: """Class containing crystal lattice information and auxiliary routines. @@ -59,8 +71,8 @@ class Crystal: def __init__( self, name: str, - lattice_params: Union[Dict[str, float], List[float]], system: str, + lattice_params: Union[Dict[str, float], List[float]], indegrees: bool = True, ) -> None: """Create a new crystal lattice and calculates B matrix. @@ -242,7 +254,6 @@ def get_lattice_params(self) -> Tuple[str, Dict[str, float]]: def _set_cell_for_system(self, params: Dict[str, float]) -> None: default_params = self.default_params - # loop through params, and replace default_params[param] with the value there. 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) @@ -301,11 +312,30 @@ def get_hkl_plane_angle( 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) + + +# def deserialise_crystal(crystal_data: JSONCrystal): +# return Crystal(**crystal_data.asdict) + # test = Crystal( # name="test", lattice_params={"a": 4.913, "c": 5.405}, system="Tetragonal" # ) # test.get_hkl_plane_angle((0, 0, 1), (0, 1, 3)) +# data = test.asdict # output = test.get_lattice_params() # print("yay") diff --git a/src/diffcalc/ub/reference.py b/src/diffcalc/ub/reference.py index 9d7ff00..f5901ef 100644 --- a/src/diffcalc/ub/reference.py +++ b/src/diffcalc/ub/reference.py @@ -1,10 +1,36 @@ """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 JSONReflection: + h: float + k: float + l: float + pos: Dict[str, Any] + energy: float + tag: str + + @property + def asdict(self): + return self.__dict__ + + +@dataclasses.dataclass +class JSONOrientation: + h: float + k: float + l: float + x: float + y: float + z: float + pos: Dict[str, Any] + tag: str + + @dataclasses.dataclass class Reflection: """Class containing reference reflection information. @@ -32,17 +58,6 @@ class Reflection: energy: float tag: str - 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)}.") - @property def astuple( self, @@ -66,6 +81,44 @@ def astuple( h, k, l, pos, en, tag = dataclasses.astuple(self) return (h, k, l), pos.astuple, en, tag + @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__ + class_info["pos"] = self.pos.asdict + return class_info + + @classmethod + def fromdict(cls, data: Dict[str, Any]) -> "Reflection": + """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["energy"], + data["tag"], + ) + class ReflectionList: """Class containing collection of reference reflections. @@ -242,6 +295,16 @@ def swap_reflections(self, idx1: Union[str, int], idx2: Union[str, int]) -> None self.reflections[num1] = self.reflections[num2] self.reflections[num2] = orig1 + @property + def asdict(self) -> List[Dict[str, Any]]: + return [ref.asdict for ref in self.reflections] + + @classmethod + def fromdict(cls, data: List[Dict[str, Any]]) -> "ReflectionList": + # for each item in the list, call Reflection.fromdict() + reflections = [Reflection.fromdict(each_ref) for each_ref in data] + return cls(reflections) + def __len__(self) -> int: """Return number of reference reflections in the list. @@ -324,17 +387,6 @@ class Orientation: pos: Position tag: str - 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)}.") - @property def astuple( self, @@ -358,6 +410,25 @@ def astuple( h, k, l, x, y, z, pos, tag = dataclasses.astuple(self) return (h, k, l), (x, y, z), pos.astuple, tag + @property + def asdict(self) -> Dict[str, Any]: + class_info = self.__dict__ + class_info["pos"] = self.pos.asdict + return class_info + + @classmethod + def fromdict(cls, data: Dict[str, Any]) -> "Orientation": + return cls( + data["h"], + data["k"], + data["l"], + data["x"], + data["y"], + data["z"], + Position(**data["pos"]), + data["tag"], + ) + class OrientationList: """Class containing collection of reference orientations. @@ -543,6 +614,15 @@ def swap_orientations(self, idx1: Union[str, int], idx2: Union[str, int]) -> Non self.orientations[num1] = self.orientations[num2] self.orientations[num2] = orig1 + @property + def asdict(self) -> List[Dict[str, Any]]: + return [ori.asdict for ori in self.orientations] + + @classmethod + def fromdict(cls, data: List[Dict[str, Any]]) -> "OrientationList": + orientations = [Orientation.fromdict(each_ref) for each_ref in data] + return cls(orientations) + def __len__(self) -> int: """Return number of reference orientations in the list. @@ -595,3 +675,26 @@ def _str_lines(self) -> List[str]: values = (n, h, k, l, x, y, z) + angles + (tag,) lines.append(str_format % values) return lines + + +# test = Reflection(1, 1, 0, Position(7, 0, 10, 0, 0, 0), 12, "wat") +# data = test.asdict + +# test2 = Reflection.fromdict(data) + +# rlist = ReflectionList([test, test2]) +# rlist_dict = rlist.asdict + +# rlist2 = ReflectionList.fromdict(rlist_dict) + +# Otest = Orientation(1, 0, 1, 1, 0, 1, Position(7, 0, 10, 0, 0, 0), "something") +# Odata = Otest.asdict + +# Otest2 = Orientation.fromdict(Odata) + +# olist = OrientationList([Otest, Otest2]) +# olist_dict = olist.asdict + +# olist2 = OrientationList.fromdict(olist_dict) + +# print("wat") From fb95f915b5924f771588514148aafa90edd96b30 Mon Sep 17 00:00:00 2001 From: Rose Yemelyanova Date: Fri, 22 Jul 2022 09:36:11 +0000 Subject: [PATCH 3/3] made changes to geometry and reference objects to simplify code. --- src/diffcalc/hkl/calc.py | 23 -- src/diffcalc/hkl/geometry.py | 198 +++--------- src/diffcalc/ub/calc.py | 51 +-- src/diffcalc/ub/crystal.py | 26 -- src/diffcalc/ub/reference.py | 587 +++++++++++++---------------------- 5 files changed, 250 insertions(+), 635 deletions(-) diff --git a/src/diffcalc/hkl/calc.py b/src/diffcalc/hkl/calc.py index 5cb090d..d1288a5 100644 --- a/src/diffcalc/hkl/calc.py +++ b/src/diffcalc/hkl/calc.py @@ -1800,26 +1800,3 @@ def fromdict(cls, data: Dict[str, Any]) -> "HklCalculation": UBCalculation.fromdict(data["ubcalc"]), Constraints(constraint_data, indegrees), ) - - -# test = UBCalculation("test") -# test.set_lattice(name="test", a=4.913, c=5.405) -# test.add_reflection( -# hkl=(0, 0, 1), -# position=Position(7.31, 0, 10.62, 0, 0, 0), -# energy=12.39842, -# tag="refl1", -# ) -# test.add_orientation(hkl=(0, 1, 0), xyz=(0, 1, 0), tag="plane") -# test.n_hkl = (1.0, 0.0, 0.0) - -# test.calc_ub("refl1", "plane") - - -# hkl = HklCalculation(test, Constraints({"qaz": 0, "alpha": 0, "eta": 0})) - -# hkldict = hkl.asdict -# hkl2 = HklCalculation.fromdict(hkldict) -# result = hkl.get_position(0, 0, 1, 0.1) - -# print("a") diff --git a/src/diffcalc/hkl/geometry.py b/src/diffcalc/hkl/geometry.py index fb1dbb0..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 Any, 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,33 +41,23 @@ class Position: If True, arguments are angles in degrees. """ - fields: Tuple[str, str, str, str, str, str, str] = ( - "mu", - "delta", - "nu", - "eta", - "chi", - "phi", - "indegrees", - ) - - 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": @@ -84,9 +73,12 @@ def asdegrees(cls, pos: "Position") -> "Position": Position New Position object with angles in degrees. """ - res = cls(**pos.asdict) - 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": @@ -102,126 +94,15 @@ def asradians(cls, pos: "Position") -> "Position": Position New Position object with angles in radians. """ - res = cls(**pos.asdict) - 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 + if pos.indegrees: + return cls( + **{key: radians(value) for key, value in pos.angles.items()}, + indegrees=False + ) + return pos @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 - - @property - def asdict(self) -> Dict[str, Any]: + def asdict(self): """Return dictionary of diffractometer angles. Returns @@ -229,11 +110,9 @@ def asdict(self) -> Dict[str, Any]: Dict[str, float] Dictionary of axis names and angle values. """ - return {field: getattr(self, field) for field in self.fields} - - # @classmethod - # def fromdict(cls, data: Mapping[str, Any]) -> "Position": - # return cls(**data) + class_info = self.angles.copy() + class_info["indegrees"] = self.indegrees + return class_info @property def astuple(self) -> Tuple[Any, ...]: @@ -244,10 +123,7 @@ def astuple(self) -> Tuple[Any, ...]: 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( diff --git a/src/diffcalc/ub/calc.py b/src/diffcalc/ub/calc.py index d3a08d4..950dec3 100644 --- a/src/diffcalc/ub/calc.py +++ b/src/diffcalc/ub/calc.py @@ -14,15 +14,9 @@ import numpy as np from diffcalc.hkl.geometry import Position, get_q_phi, get_rotation_matrices -from diffcalc.ub.crystal import Crystal, JSONCrystal +from diffcalc.ub.crystal import Crystal from diffcalc.ub.fitting import fit_crystal, fit_u_matrix -from diffcalc.ub.reference import ( - JSONOrientation, - JSONReflection, - OrientationList, - Reflection, - ReflectionList, -) +from diffcalc.ub.reference import OrientationList, Reflection, ReflectionList from diffcalc.ub.systems import available_systems from diffcalc.util import ( SMALL, @@ -37,28 +31,6 @@ from numpy.linalg import inv, norm -@dataclasses.dataclass -class JSONReferenceVector: - n_ref: Tuple[float, float, float] - rlv: bool - - @property - def asdict(self): - return self.__dict__ - - -@dataclasses.dataclass -class JSONUBCalculation: - name: str - crystal: Optional[JSONCrystal] - reflist: List[JSONReflection] - orientlist: List[JSONOrientation] - reference: Dict[str, Any] - surface: Dict[str, Any] - u_matrix: Optional[List[List[float]]] - ub_matrix: Optional[List[List[float]]] - - @dataclasses.dataclass class ReferenceVector: """Class representing reference vector information. @@ -1426,22 +1398,3 @@ def fromdict(cls, data: Dict[str, Any]) -> "UBCalculation": np.array(data["ub_matrix"]) if data["ub_matrix"] is not None else None ) return ubcalc - - -# test = UBCalculation("test") -# test.set_lattice(name="test", a=4.913, c=5.405) -# test.add_reflection( -# hkl=(0, 0, 1), -# position=Position(7.31, 0, 10.62, 0, 0, 0), -# energy=12.39842, -# tag="refl1", -# ) -# test.add_orientation(hkl=(0, 1, 0), xyz=(0, 1, 0), tag="plane") -# test.n_hkl = (1.0, 0.0, 0.0) - -# # test.calc_ub() - -# tdict = test.asdict - -# test2 = UBCalculation.fromdict(tdict) -# print("a") diff --git a/src/diffcalc/ub/crystal.py b/src/diffcalc/ub/crystal.py index 766777c..05368bc 100644 --- a/src/diffcalc/ub/crystal.py +++ b/src/diffcalc/ub/crystal.py @@ -3,7 +3,6 @@ A module defining crystal lattice class and auxiliary methods for calculating crystal plane geometric properties. """ -from dataclasses import dataclass from math import acos, cos, degrees, pi, radians, sin, sqrt from typing import Any, Dict, List, Tuple, Union @@ -19,17 +18,6 @@ def lists_equal(list1: List[Any], list2: List[Any]) -> bool: return bool(np.all([item in list2 for item in list1])) -@dataclass -class JSONCrystal: - name: str - system: str - lattice_params: Dict[str, float] - - @property - def asdict(self): - return self.__dict__ - - class Crystal: """Class containing crystal lattice information and auxiliary routines. @@ -325,17 +313,3 @@ def asdict(self) -> Dict[str, Any]: @classmethod def fromdict(cls, data: Dict[str, Any]) -> "Crystal": return Crystal(**data) - - -# def deserialise_crystal(crystal_data: JSONCrystal): -# return Crystal(**crystal_data.asdict) - - -# test = Crystal( -# name="test", lattice_params={"a": 4.913, "c": 5.405}, system="Tetragonal" -# ) -# test.get_hkl_plane_angle((0, 0, 1), (0, 1, 3)) -# data = test.asdict - -# output = test.get_lattice_params() -# print("yay") diff --git a/src/diffcalc/ub/reference.py b/src/diffcalc/ub/reference.py index f5901ef..dbe36de 100644 --- a/src/diffcalc/ub/reference.py +++ b/src/diffcalc/ub/reference.py @@ -6,33 +6,24 @@ @dataclasses.dataclass -class JSONReflection: +class Reference: h: float k: float l: float - pos: Dict[str, Any] - energy: float + pos: Position tag: str @property - def asdict(self): - return self.__dict__ - + def asdict(self) -> Dict[str, Any]: + return {} -@dataclasses.dataclass -class JSONOrientation: - h: float - k: float - l: float - x: float - y: float - z: float - pos: Dict[str, Any] - tag: str + @property + def astuple(self) -> Tuple[Any, ...]: + return () @dataclasses.dataclass -class Reflection: +class Reflection(Reference): """Class containing reference reflection information. Attributes @@ -51,35 +42,25 @@ class Reflection: Identifying tag for the reflection. """ - h: float - k: float - l: float - pos: Position energy: float - tag: str @property def astuple( self, - ) -> Tuple[ - Tuple[float, float, float], - Tuple[float, float, float, float, float, float], - float, - str, - ]: + ) -> Tuple[Tuple[float, float, float], Tuple[Any, ...], float, str,]: """Return reference reflection data as tuple. Returns ------- Tuple[Tuple[float, float, float], - Tuple[float, float, float, float, float, float], + Tuple[Any, ...], float, str] Tuple containing miller indices, position object, energy and reflection tag. """ - h, k, l, pos, en, tag = dataclasses.astuple(self) - return (h, k, l), pos.astuple, en, tag + h, k, l, pos, tag, en = dataclasses.astuple(self) + return (h, k, l), pos, tag, en @property def asdict(self) -> Dict[str, Any]: @@ -91,12 +72,12 @@ def asdict(self) -> Dict[str, Any]: Class structure containing miller indices, position as a dictionary, energy and reflection tag. """ - class_info = self.__dict__ + class_info = self.__dict__.copy() class_info["pos"] = self.pos.asdict return class_info @classmethod - def fromdict(cls, data: Dict[str, Any]) -> "Reflection": + def fromdict(cls, data: Dict[str, Any]) -> Reference: """Create reflection object from a dictionary. Parameters @@ -115,22 +96,85 @@ def fromdict(cls, data: Dict[str, Any]) -> "Reflection": data["k"], data["l"], Position(**data["pos"]), - data["energy"], data["tag"], + data["energy"], ) -class ReflectionList: - """Class containing collection of reference reflections. +@dataclasses.dataclass +class Orientation(Reference): + """Class containing reference orientation information. Attributes ---------- - reflections: List[Reflection] - List containing reference reflections. + 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 __init__(self, reflections=None): - self.reflections: List[Reflection] = reflections if reflections else [] + 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], + str, + ]: + """Return reference orientation data as tuple. + + 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. + """ + 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"], + ) + + +@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. @@ -152,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 ------- @@ -235,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 ---------- @@ -264,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 ---------- @@ -291,19 +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.reflections] - - @classmethod - def fromdict(cls, data: List[Dict[str, Any]]) -> "ReflectionList": - # for each item in the list, call Reflection.fromdict() - reflections = [Reflection.fromdict(each_ref) for each_ref in data] - return cls(reflections) + return [ref.asdict for ref in self.items] def __len__(self) -> int: """Return number of reference reflections in the list. @@ -313,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. @@ -325,6 +302,83 @@ def __str__(self) -> str: """ return "\n".join(self._str_lines()) + def _str_lines(self) -> List[str]: + return [] + + +class ReflectionList(RefOrientList): + """Class containing collection of reference reflections. + + Attributes + ---------- + reflections: List[Reflection] + List containing reference reflections. + """ + + 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. + + 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)] + + def edit_item( + 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 + + 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. @@ -333,104 +387,33 @@ def _str_lines(self) -> List[str]: List[str] List containing reference reflection table rows. """ - axes = tuple(fd.upper() for fd in Position.fields) - if not self.reflections: + 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) + " TAG" - header_values = ("ENERGY", "H", "K", "L") + axes + 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(1, len(self.reflections) + 1): - ref_tuple = self.get_reflection(n) - (h, k, l), pos, energy, tag = ref_tuple.astuple + 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) + " %s" + 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 -@dataclasses.dataclass -class Orientation: - """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 - Diffractometer position object. - tag: str - Identifying tag for the orientation. - """ - - h: float - k: float - l: float - x: float - y: float - z: float - pos: Position - tag: str - - @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. - - 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. - """ - h, k, l, x, y, z, pos, tag = dataclasses.astuple(self) - return (h, k, l), (x, y, z), pos.astuple, tag - - @property - def asdict(self) -> Dict[str, Any]: - class_info = self.__dict__ - class_info["pos"] = self.pos.asdict - return class_info - - @classmethod - def fromdict(cls, data: Dict[str, Any]) -> "Orientation": - return cls( - data["h"], - data["k"], - data["l"], - data["x"], - data["y"], - data["z"], - Position(**data["pos"]), - data["tag"], - ) - - -class OrientationList: +class OrientationList(RefOrientList): """Class containing collection of reference orientations. Attributes @@ -439,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], @@ -490,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], @@ -531,118 +487,14 @@ 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. - - 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 - - @property - def asdict(self) -> List[Dict[str, Any]]: - return [ori.asdict for ori in self.orientations] + self.items[num] = Orientation(*hkl, pos, tag, *xyz) @classmethod def fromdict(cls, data: List[Dict[str, Any]]) -> "OrientationList": orientations = [Orientation.fromdict(each_ref) for each_ref in data] return cls(orientations) - 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()) - def _str_lines(self) -> List[str]: """Table with reference orientations data. @@ -651,50 +503,33 @@ 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,) lines.append(str_format % values) return lines - - -# test = Reflection(1, 1, 0, Position(7, 0, 10, 0, 0, 0), 12, "wat") -# data = test.asdict - -# test2 = Reflection.fromdict(data) - -# rlist = ReflectionList([test, test2]) -# rlist_dict = rlist.asdict - -# rlist2 = ReflectionList.fromdict(rlist_dict) - -# Otest = Orientation(1, 0, 1, 1, 0, 1, Position(7, 0, 10, 0, 0, 0), "something") -# Odata = Otest.asdict - -# Otest2 = Orientation.fromdict(Odata) - -# olist = OrientationList([Otest, Otest2]) -# olist_dict = olist.asdict - -# olist2 = OrientationList.fromdict(olist_dict) - -# print("wat")