######################################################################
# BioSimSpace: Making biomolecular simulation a breeze!
#
# Copyright: 2017-2024
#
# Authors: Lester Hedges <lester.hedges@gmail.com>
#
# BioSimSpace is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BioSimSpace is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BioSimSpace. If not, see <http://www.gnu.org/licenses/>.
#####################################################################
"""An angle type."""
__author__ = "Lester Hedges"
__email__ = "lester.hedges@gmail.com"
__all__ = ["Angle"]
from sire.legacy import Units as _SireUnits
from ._type import Type as _Type
[docs]
class Angle(_Type):
"""An angle type."""
# A list of the supported Sire unit names.
_sire_units = ["radian", "degree"]
# Dictionary of allowed units.
_supported_units = {"RADIAN": _SireUnits.radian, "DEGREE": _SireUnits.degree}
# Map unit abbreviations to the full name.
_abbreviations = {"R": "RADIAN", "D": "DEGREE"}
# Print format.
_print_format = {"RADIAN": "radian", "DEGREE": "degree"}
# Documentation strings.
_doc_strings = {"RADIAN": "An angle in radians.", "DEGREE": "An angle in degrees."}
# Null type unit for avoiding issue printing configargparse help.
_default_unit = "RADIAN"
# The dimension mask.
_dimensions = tuple(list(_supported_units.values())[0].dimensions())
[docs]
def __init__(self, *args):
"""
Constructor.
``*args`` can be a value and unit, or a string representation
of the angle, e.g. "3 radians".
Parameters
----------
value : float
The value.
unit : str
The unit.
string : str
A string representation of the angle.
Examples
--------
Create an object representing an angle of 3.14 radians then
print the length in degrees.
>>> import BioSimSpace as BSS
>>> length = BSS.Types.Angle(3.14, "R")
>>> print(length.degrees())
The same as above, except passing a string representation of the
angle to the constructor.
>>> import BioSimSpace as BSS
>>> length = BSS.Types.Angle("3.14 R")
>>> print(length.degrees())
The string matching is extremeley flexible, so all of the following
would be valid arguments: "3.14 R", "3.14 radians", "314e-2 Radians".
"""
# Call the base class constructor.
super().__init__(*args)
def __str__(self):
"""Return a human readable string representation of the object."""
abbrev = self._print_format[self._unit]
if self._value != 1:
if abbrev[-1] != "s":
abbrev = abbrev + "s"
if abs(self._value) > 1e4 or abs(self._value) < 1e-4:
return "%.4e %s" % (self._value, abbrev)
else:
return "%5.4f %s" % (self._value, abbrev)
[docs]
def radians(self):
"""
Return the angle in radians.
Returns
-------
angle : :class:`Angle <BioSimSpace.Types.Angle>`
The angle in radians.
"""
return Angle(
(self._value * self._supported_units[self._unit]).to(_SireUnits.radian),
"RADIAN",
)
[docs]
def degrees(self):
"""
Return the angle in degrees.
Returns
-------
angle : :class:`Angle <BioSimSpace.Types.Angle>`
The angle in degrees.
"""
return Angle(
(self._value * self._supported_units[self._unit]).to(_SireUnits.degree),
"DEGREE",
)
def _to_default_unit(self, mag=None):
"""
Internal method to return an object of the same type in the default unit.
Parameters
----------
mag : float
The value (optional).
Returns
-------
angle : :class:`Angle <BioSimSpace.Types.Angle>`
The length in the default unit of radians.
"""
if mag is None:
return self.radians()
else:
return Angle(mag, "RADIAN")
def _convert_to(self, unit):
"""
Return the angle in a different unit.
Parameters
----------
unit : str
The unit to convert to.
Returns
-------
angle : :class:`Angle <BioSimSpace.Types.Angle>`
The angle in the specified unit.
"""
if unit == "RADIAN":
return self.radians()
elif unit == "DEGREE":
return self.degrees()
else:
raise ValueError(
"Supported units are: '%s'" % list(self._supported_units.keys())
)
@classmethod
def _validate_unit(cls, unit):
"""Validate that the unit are supported."""
# Strip whitespace and convert to upper case.
unit = unit.replace(" ", "").upper()
# Strip any "S" characters.
unit = unit.replace("S", "")
# Strip "EGREE".
unit = unit.replace("EGREE", "")
# Strip "EG".
unit = unit.replace("EG", "")
# Strip "ADIAN".
unit = unit.replace("ADIAN", "")
# Strip "AD".
unit = unit.replace("AD", "")
# Check that the unit is supported.
if unit in cls._supported_units:
return unit
elif unit in cls._abbreviations:
return cls._abbreviations[unit]
else:
raise ValueError(
"Supported units are: '%s'" % list(cls._supported_units.keys())
)
@staticmethod
def _to_sire_format(unit):
"""
Reformat the unit string so it adheres to the Sire unit formatting.
Parameters
----------
unit : str
A string representation of the unit.
Returns
-------
sire_unit : str
The unit string in Sire compatible format.
"""
# First, handle plurals and abbreviations.
unit = unit.replace("radians", "rad")
unit = unit.replace("radian", "rad")
unit = unit.replace("rads", "rad")
# Now convert back to correct format.
unit = unit.replace("rad", "radian")
# Convert powers. (Limited selection, for now.)
unit = unit.replace("radian2", "(radian*radian)")
unit = unit.replace("radian3", "(radian*radian*radian)")
unit = unit.replace("degree2", "(degree*degree)")
unit = unit.replace("degree3", "(degree*degree*degree)")
unit = unit.replace("radian-1", "(1/(radian))")
unit = unit.replace("radian-2", "(1/(radian*radian))")
unit = unit.replace("radian-3", "(1/(radian*radian*radian))")
unit = unit.replace("degree-1", "(1/(degree))")
unit = unit.replace("degree-2", "(1/(degree*degree))")
unit = unit.replace("degree-3", "(1/(degree*degree*degree))")
return unit