diff --git a/PQAnalysis/core/__init__.py b/PQAnalysis/core/__init__.py index cb350496..95b6e917 100644 --- a/PQAnalysis/core/__init__.py +++ b/PQAnalysis/core/__init__.py @@ -17,6 +17,7 @@ from .cell import Cell, Cells from .exceptions import ( AtomError, + CellError, ElementNotFoundError, ResidueError, ResidueWarning, @@ -28,6 +29,7 @@ "ResidueError": ".exceptions", "ResidueWarning": ".exceptions", "AtomError": ".exceptions", + "CellError": ".exceptions", "Cell": ".cell", "Cells": ".cell", "Atom": ".atom", diff --git a/PQAnalysis/core/cell/cell.py b/PQAnalysis/core/cell/cell.py index 77abf932..0aa1f2eb 100644 --- a/PQAnalysis/core/cell/cell.py +++ b/PQAnalysis/core/cell/cell.py @@ -2,6 +2,7 @@ A module containing the Cell class. """ +import logging import sys import warnings @@ -14,9 +15,12 @@ from PQAnalysis.type_checking import runtime_type_checking from PQAnalysis.types import Np3x3NumberArray, Np2DNumberArray, NpnDNumberArray, PositiveReal, Bool +from PQAnalysis.utils.custom_logging import setup_logger from PQAnalysis.utils.math import allclose_vectorized +from PQAnalysis import __package_name__ from ._standard_properties import _StandardPropertiesMixin +from ..exceptions import CellError @@ -26,6 +30,9 @@ class Cell(_StandardPropertiesMixin): Class for storing unit cell parameters. """ + logger = logging.getLogger(__package_name__).getChild(__qualname__) + logger = setup_logger(logger) + @runtime_type_checking def __init__( self, @@ -72,7 +79,30 @@ def setup_box_matrix(self) -> Np3x3NumberArray: ------- box matrix: Np3x3NumberArray The box matrix. + + Raises + ------ + CellError + If a box length is not positive, a box angle is not + within the interval (0, 180) or the box angles do not + define a valid unit cell. """ + if not np.all(np.greater(self.box_lengths, 0)): + self.logger.error( + f"Box lengths must be positive, but got {self.box_lengths}.", + exception=CellError + ) + + if not ( + np.all(np.greater(self.box_angles, 0)) and + np.all(np.less(self.box_angles, 180)) + ): + self.logger.error( + "Box angles must be within the interval (0, 180) degrees, " + f"but got {self.box_angles}.", + exception=CellError + ) + matrix = np.array([[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]]) alpha, beta, gamma = np.deg2rad(self.box_angles) @@ -81,14 +111,23 @@ def setup_box_matrix(self) -> Np3x3NumberArray: sin_beta = np.sin(beta) x, y, z = self.box_lengths + radicand = ( + sin_beta**2 - (cos_alpha - cos_beta * cos_gamma)**2 / sin_gamma**2 + ) + + if not radicand > 0: + self.logger.error( + f"Box angles {self.box_angles} do not define a valid " + "unit cell.", + exception=CellError + ) + matrix[0][0] = x matrix[0][1] = y * cos_gamma matrix[0][2] = z * cos_beta matrix[1][1] = y * sin_gamma matrix[1][2] = z * (cos_alpha - cos_beta * cos_gamma) / sin_gamma - matrix[2][2] = z * np.sqrt( - sin_beta**2 - (cos_alpha - cos_beta * cos_gamma)**2 / sin_gamma**2 - ) + matrix[2][2] = z * np.sqrt(radicand) return matrix diff --git a/PQAnalysis/core/exceptions.py b/PQAnalysis/core/exceptions.py index c2731ca1..52570c82 100644 --- a/PQAnalysis/core/exceptions.py +++ b/PQAnalysis/core/exceptions.py @@ -68,3 +68,11 @@ class AtomError(PQException): """ Exception raised for errors related to the Atom class """ + + + +class CellError(PQException): + + """ + Exception raised for errors related to the Cell class + """ diff --git a/tests/core/test_cell.py b/tests/core/test_cell.py index 4e485cc7..12e795ca 100644 --- a/tests/core/test_cell.py +++ b/tests/core/test_cell.py @@ -2,8 +2,9 @@ import pytest from . import pytestmark +from ..conftest import assert_logging_with_exception -from PQAnalysis.core import Cell +from PQAnalysis.core import Cell, CellError @@ -35,6 +36,67 @@ def test__init__(self): ) ) + def test__init__non_positive_box_lengths(self, caplog): + assert_logging_with_exception( + caplog, + Cell.__qualname__, + "ERROR", + "Box lengths must be positive, but got [0. 0. 0.].", + CellError, + Cell, + 0.0, + 0.0, + 0.0 + ) + + def test__init__box_angles_out_of_range(self, caplog): + assert_logging_with_exception( + caplog, + Cell.__qualname__, + "ERROR", + "Box angles must be within the interval (0, 180) degrees, " + "but got [190. 90. 90.].", + CellError, + Cell, + 10.0, + 10.0, + 10.0, + 190.0, + 90.0, + 90.0 + ) + + def test__init__impossible_box_angles(self, caplog): + assert_logging_with_exception( + caplog, + Cell.__qualname__, + "ERROR", + "Box angles [170. 10. 10.] do not define a valid unit cell.", + CellError, + Cell, + 10.0, + 10.0, + 10.0, + 170.0, + 10.0, + 10.0 + ) + + def test_set_box_lengths_non_positive(self, caplog): + cell = Cell(1, 2, 3) + + def set_box_lengths(): + cell.box_lengths = np.array([0.0, 2.0, 3.0]) + + assert_logging_with_exception( + caplog, + Cell.__qualname__, + "ERROR", + "Box lengths must be positive, but got [0. 2. 3.].", + CellError, + set_box_lengths + ) + def test_box_lengths(self): cell = Cell(1, 2, 3) assert np.allclose(cell.box_lengths, np.array([1, 2, 3]))