diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index 5420e438a..0e8765d6c 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -9,6 +9,19 @@ company supporting open-source development of fostering academic/industrial coll within the biomolecular simulation community. Our software is hosted via the `OpenBioSim` `GitHub `__ organisation. +`2023.5.1 `_ - Mar 20 2024 +------------------------------------------------------------------------------------------------- + +* Fixed path to user links file in the :func:`generateNetwork ` function (`#233 `__). +* Fixed redirection of stderr (`#233 `__). +* Switched to using ``AtomCoordMatcher`` to map parameterised molecules back to their original topology. This resolves issues where atoms moved between residues following parameterisation (`#235 `__). +* Make the GROMACS ``_generate_binary_run_file`` function static so that it can be used when initialising free energy simulations in setup-only mode (`#237 `__). +* Improve error handling and message when attempting to extract an all dummy atom selection (`#251 `__). +* Don't set SOMD specific end-state properties when decoupling a molecule (`#253 `__). +* Only convert to a end-state system when not running a free energy protocol with GROMACS so that hybrid topology isn't lost when using position restraints (`#257 `__). +* Exclude standard free ions from the AMBER position restraint mask (`#260 `__). +* Update the ``BioSimSpace.Types._GeneralUnit.__pow__`` operator to support fractional exponents (`#260 `__). + `2023.5.0 `_ - Dec 16 2023 ------------------------------------------------------------------------------------------------- diff --git a/python/BioSimSpace/Process/_gromacs.py b/python/BioSimSpace/Process/_gromacs.py index e4531dad3..166910e05 100644 --- a/python/BioSimSpace/Process/_gromacs.py +++ b/python/BioSimSpace/Process/_gromacs.py @@ -1995,8 +1995,10 @@ def _add_position_restraints(self): # Create a copy of the system. system = self._system.copy() - # Convert to the lambda = 0 state if this is a perturbable system. - system = self._checkPerturbable(system) + # Convert to the lambda = 0 state if this is a perturbable system and this + # isn't a free energy protocol. + if not isinstance(self._protocol, _FreeEnergyMixin): + system = self._checkPerturbable(system) # Convert the water model topology so that it matches the GROMACS naming convention. system._set_water_topology("GROMACS") diff --git a/python/BioSimSpace/Protocol/_position_restraint_mixin.py b/python/BioSimSpace/Protocol/_position_restraint_mixin.py index 346f7f6e3..8374bf8ad 100644 --- a/python/BioSimSpace/Protocol/_position_restraint_mixin.py +++ b/python/BioSimSpace/Protocol/_position_restraint_mixin.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -214,7 +214,7 @@ def setForceConstant(self, force_constant): ) # Validate the dimensions. - if force_constant.dimensions() != (0, 0, 0, 1, -1, 0, -2): + if force_constant.dimensions() != (1, 0, -2, 0, 0, -1, 0): raise ValueError( "'force_constant' has invalid dimensions! " f"Expected dimensions are 'M Q-1 T-2', found '{force_constant.unit()}'" diff --git a/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint.py b/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint.py index 499846255..ab961ddb2 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint.py +++ b/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -174,7 +174,7 @@ def __init__(self, system, restraint_dict, temperature, restraint_type="Boresch" for key in ["kthetaA", "kthetaB", "kphiA", "kphiB", "kphiC"]: if restraint_dict["force_constants"][key] != 0: dim = restraint_dict["force_constants"][key].dimensions() - if dim != (-2, 0, 2, 1, -1, 0, -2): + if dim != (1, 2, -2, 0, 0, -1, -2): raise ValueError( f"restraint_dict['force_constants']['{key}'] must be of type " f"'BioSimSpace.Types.Energy'/'BioSimSpace.Types.Angle^2'" @@ -202,7 +202,7 @@ def __init__(self, system, restraint_dict, temperature, restraint_type="Boresch" # Test if the force constant of the bond r1-l1 is the correct unit # Such as kcal/mol/angstrom^2 dim = restraint_dict["force_constants"]["kr"].dimensions() - if dim != (0, 0, 0, 1, -1, 0, -2): + if dim != (1, 0, -2, 0, 0, -1, 0): raise ValueError( "restraint_dict['force_constants']['kr'] must be of type " "'BioSimSpace.Types.Energy'/'BioSimSpace.Types.Length^2'" @@ -290,13 +290,13 @@ def __init__(self, system, restraint_dict, temperature, restraint_type="Boresch" "'BioSimSpace.Types.Length'" ) if not single_restraint_dict["kr"].dimensions() == ( + 1, 0, + -2, 0, 0, - 1, -1, 0, - -2, ): raise ValueError( "distance_restraint_dict['kr'] must be of type " diff --git a/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint_search.py b/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint_search.py index 30c4b0af4..f7ec62dd4 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint_search.py +++ b/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint_search.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -641,7 +641,7 @@ def analyse( if force_constant: dim = force_constant.dimensions() - if dim != (0, 0, 0, 1, -1, 0, -2): + if dim != (1, 0, -2, 0, 0, -1, 0): raise ValueError( "force_constant must be of type " "'BioSimSpace.Types.Energy'/'BioSimSpace.Types.Length^2'" diff --git a/python/BioSimSpace/Sandpit/Exscientia/Process/_gromacs.py b/python/BioSimSpace/Sandpit/Exscientia/Process/_gromacs.py index 6d0bf4278..6c17685f4 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Process/_gromacs.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Process/_gromacs.py @@ -2099,8 +2099,10 @@ def _add_position_restraints(self, config_options): # Create a copy of the system. system = self._system.copy() - # Convert to the lambda = 0 state if this is a perturbable system. - system = self._checkPerturbable(system) + # Convert to the lambda = 0 state if this is a perturbable system and this + # isn't a free energy protocol. + if not isinstance(self._protocol, _Protocol._FreeEnergyMixin): + system = self._checkPerturbable(system) # Convert the water model topology so that it matches the GROMACS naming convention. system._set_water_topology("GROMACS") diff --git a/python/BioSimSpace/Sandpit/Exscientia/Protocol/_config.py b/python/BioSimSpace/Sandpit/Exscientia/Protocol/_config.py index 4496107d7..a08935d9b 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Protocol/_config.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Protocol/_config.py @@ -293,9 +293,9 @@ def generateAmberConfig(self, extra_options=None, extra_lines=None): ] restraint_mask = "@" + ",".join(restraint_atom_names) elif restraint == "heavy": - restraint_mask = "!:WAT & !@H=" + restraint_mask = "!:WAT & !@%NA,CL & !@H=" elif restraint == "all": - restraint_mask = "!:WAT" + restraint_mask = "!:WAT & !@%NA,CL" # We can't do anything about a custom restraint, since we don't # know anything about the atoms. diff --git a/python/BioSimSpace/Sandpit/Exscientia/Protocol/_position_restraint.py b/python/BioSimSpace/Sandpit/Exscientia/Protocol/_position_restraint.py index ae3578870..38a82266d 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Protocol/_position_restraint.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Protocol/_position_restraint.py @@ -185,7 +185,7 @@ def setForceConstant(self, force_constant): ) # Validate the dimensions. - if force_constant.dimensions() != (0, 0, 0, 1, -1, 0, -2): + if force_constant.dimensions() != (1, 0, -2, 0, 0, -1, 0): raise ValueError( "'force_constant' has invalid dimensions! " f"Expected dimensions are 'M Q-1 T-2', found '{force_constant.unit()}'" diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_angle.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_angle.py index 759a71724..cef19c862 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_angle.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_angle.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -52,9 +52,8 @@ class Angle(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "RADIAN" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (1, 0, 0, 0, 0, 0, 0) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ @@ -188,7 +187,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. @@ -210,13 +210,13 @@ def _validate_unit(self, unit): unit = unit.replace("AD", "") # Check that the unit is supported. - if unit in self._supported_units: + if unit in cls._supported_units: return unit - elif unit in self._abbreviations: - return self._abbreviations[unit] + elif unit in cls._abbreviations: + return cls._abbreviations[unit] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_area.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_area.py index ec8484cca..2763618a9 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_area.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_area.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -72,9 +72,8 @@ class Area(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "ANGSTROM2" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 0, 2, 0, 0, 0, 0) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ @@ -330,7 +329,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit is supported.""" # Strip whitespace and convert to upper case. @@ -360,13 +360,13 @@ def _validate_unit(self, unit): unit = unit[0:index] + unit[index + 1 :] + "2" # Check that the unit is supported. - if unit in self._supported_units: + if unit in cls._supported_units: return unit - elif unit in self._abbreviations: - return self._abbreviations[unit] + elif unit in cls._abbreviations: + return cls._abbreviations[unit] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_charge.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_charge.py index 7717f481e..a65d54cd3 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_charge.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_charge.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -58,9 +58,8 @@ class Charge(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "ELECTRON CHARGE" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 1, 0, 0, 0, 0, 0) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ @@ -182,7 +181,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. @@ -213,11 +213,11 @@ def _validate_unit(self, unit): unit = unit.replace("COUL", "C") # Check that the unit is supported. - if unit in self._abbreviations: - return self._abbreviations[unit] + if unit in cls._abbreviations: + return cls._abbreviations[unit] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_energy.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_energy.py index bb293a17a..af9aa2894 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_energy.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_energy.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -68,9 +68,8 @@ class Energy(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "KILO CALORIES PER MOL" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 0, 2, 1, -1, 0, -2) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ @@ -213,7 +212,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. @@ -235,11 +235,11 @@ def _validate_unit(self, unit): unit = unit.replace("JOULES", "J") # Check that the unit is supported. - if unit in self._abbreviations: - return self._abbreviations[unit] + if unit in cls._abbreviations: + return cls._abbreviations[unit] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_general_unit.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_general_unit.py index deca60800..7097e616e 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_general_unit.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_general_unit.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -38,16 +38,16 @@ class GeneralUnit(_Type): """A general unit type.""" _dimension_chars = [ - "A", # Angle - "C", # Charge - "L", # Length "M", # Mass - "Q", # Quantity + "L", # Length + "T", # Time + "C", # Charge "t", # Temperature - "T", # Tme + "Q", # Quantity + "A", # Angle ] - def __new__(cls, *args): + def __new__(cls, *args, no_cast=False): """ Constructor. @@ -65,6 +65,9 @@ def __new__(cls, *args): string : str A string representation of the unit type. + + no_cast: bool + Whether to disable casting to a specific type. """ # This operator may be called when unpickling an object. Catch empty @@ -96,7 +99,7 @@ def __new__(cls, *args): if isinstance(_args[0], _GeneralUnit): general_unit = _args[0] - # The user has passed a string representation of the temperature. + # The user has passed a string representation of the type. elif isinstance(_args[0], str): # Extract the string. string = _args[0] @@ -128,15 +131,7 @@ def __new__(cls, *args): general_unit = value * general_unit # Store the dimension mask. - dimensions = ( - general_unit.ANGLE(), - general_unit.CHARGE(), - general_unit.LENGTH(), - general_unit.MASS(), - general_unit.QUANTITY(), - general_unit.TEMPERATURE(), - general_unit.TIME(), - ) + dimensions = tuple(general_unit.dimensions()) # This is a dimensionless quantity, return the value as a float. if all(x == 0 for x in dimensions): @@ -144,13 +139,13 @@ def __new__(cls, *args): # Check to see if the dimensions correspond to a supported type. # If so, return an object of that type. - if dimensions in _base_dimensions: + if not no_cast and dimensions in _base_dimensions: return _base_dimensions[dimensions](general_unit) # Otherwise, call __init__() else: return super(GeneralUnit, cls).__new__(cls) - def __init__(self, *args): + def __init__(self, *args, no_cast=False): """ Constructor. @@ -168,6 +163,9 @@ def __init__(self, *args): string : str A string representation of the unit type. + + no_cast: bool + Whether to disable casting to a specific type. """ value = 1 @@ -194,7 +192,7 @@ def __init__(self, *args): if isinstance(_args[0], _GeneralUnit): general_unit = _args[0] - # The user has passed a string representation of the temperature. + # The user has passed a string representation of the type. elif isinstance(_args[0], str): # Extract the string. string = _args[0] @@ -222,15 +220,7 @@ def __init__(self, *args): self._value = self._sire_unit.value() # Store the dimension mask. - self._dimensions = ( - general_unit.ANGLE(), - general_unit.CHARGE(), - general_unit.LENGTH(), - general_unit.MASS(), - general_unit.QUANTITY(), - general_unit.TEMPERATURE(), - general_unit.TIME(), - ) + self._dimensions = tuple(general_unit.dimensions()) # Create the unit string. self._unit = "" @@ -271,12 +261,22 @@ def __add__(self, other): temp = self._from_string(other) return self + temp + # Addition of a zero-valued integer or float. + elif isinstance(other, (int, float)) and other == 0: + return self + else: raise TypeError( "unsupported operand type(s) for +: '%s' and '%s'" % (self.__class__.__qualname__, other.__class__.__qualname__) ) + def __radd__(self, other): + """Addition operator.""" + + # Addition is commutative: a+b = b+a + return self.__add__(other) + def __sub__(self, other): """Subtraction operator.""" @@ -285,17 +285,27 @@ def __sub__(self, other): temp = self._sire_unit - other._to_sire_unit() return GeneralUnit(temp) - # Addition of a string. + # Subtraction of a string. elif isinstance(other, str): temp = self._from_string(other) return self - temp + # Subtraction of a zero-valued integer or float. + elif isinstance(other, (int, float)) and other == 0: + return self + else: raise TypeError( "unsupported operand type(s) for -: '%s' and '%s'" % (self.__class__.__qualname__, other.__class__.__qualname__) ) + def __rsub__(self, other): + """Subtraction operator.""" + + # Subtraction is not commutative: a-b != b-a + return -self.__sub__(other) + def __mul__(self, other): """Multiplication operator.""" @@ -312,16 +322,8 @@ def __mul__(self, other): # Multipy the Sire unit objects. temp = self._sire_unit * other._to_sire_unit() - # Create the dimension mask. - dimensions = ( - temp.ANGLE(), - temp.CHARGE(), - temp.LENGTH(), - temp.MASS(), - temp.QUANTITY(), - temp.TEMPERATURE(), - temp.TIME(), - ) + # Get the dimension mask. + dimensions = temp.dimensions() # Return as an existing type if the dimensions match. try: @@ -432,7 +434,7 @@ def __rtruediv__(self, other): def __pow__(self, other): """Power operator.""" - if type(other) is not int: + if not isinstance(other, (int, float)): raise TypeError( "unsupported operand type(s) for ^: '%s' and '%s'" % (self.__class__.__qualname__, other.__class__.__qualname__) @@ -441,15 +443,29 @@ def __pow__(self, other): if other == 0: return GeneralUnit(self._sire_unit / self._sire_unit) - # Multiply the Sire GeneralUnit 'other' times. - temp = self._sire_unit - for x in range(0, abs(other) - 1): - temp = temp * self._sire_unit + # Convert to float. + other = float(other) - if other > 0: - return GeneralUnit(temp) - else: - return GeneralUnit(1 / temp) + # Get the existing unit dimensions. + dims = self.dimensions() + + # Compute the new dimensions, rounding floats to 16 decimal places. + new_dims = [round(dim * other, 16) for dim in dims] + + # Make sure the new dimensions are integers. + if not all(dim.is_integer() for dim in new_dims): + raise ValueError( + "The exponent must be a factor of all the unit dimensions." + ) + + # Convert to integers. + new_dims = [int(dim) for dim in new_dims] + + # Compute the new value. + value = self.value() ** other + + # Return a new GeneralUnit object. + return GeneralUnit(_GeneralUnit(value, new_dims)) def __lt__(self, other): """Less than operator.""" @@ -606,87 +622,87 @@ def dimensions(self): """ return self._dimensions - def angle(self): + def mass(self): """ - Return the power of this general unit in the 'angle' dimension. + Return the power of this general unit in the 'mass' dimension. Returns ------- - angle : int - The power of the general unit in the 'angle' dimension. + mass : int + The power of the general unit in the 'mass' dimension. """ return self._dimensions[0] - def charge(self): + def length(self): """ - Return the power of this general unit in the 'charge' dimension. + Return the power of this general unit in the 'length' dimension. Returns ------- - charge : int - The power of the general unit in the 'charge' dimension. + length : int + The power of the general unit in the 'length' dimension. """ return self._dimensions[1] - def length(self): + def time(self): """ - Return the power of this general unit in the 'length' dimension. + Return the power of this general unit in the 'time' dimension. Returns ------- - length : int - The power of the general unit in the 'length' dimension. + time : int + The power of the general unit in the 'time' dimension. """ return self._dimensions[2] - def mass(self): + def charge(self): """ - Return the power of this general unit in the 'mass' dimension. + Return the power of this general unit in the 'charge' dimension. Returns ------- - mass : int - The power of the general unit in the 'mass' dimension. + charge : int + The power of the general unit in the 'charge' dimension. """ return self._dimensions[3] - def quantity(self): + def temperature(self): """ - Return the power of this general unit in the 'quantity' dimension. + Return the power of this general unit in the 'temperature' dimension. Returns ------- - quantity : int - The power of the general unit in the 'quantity' dimension. + temperature : int + The power of the general unit in the 'temperature' dimension. """ return self._dimensions[4] - def temperature(self): + def quantity(self): """ - Return the power of this general unit in the 'temperature' dimension. + Return the power of this general unit in the 'quantity' dimension. Returns ------- - temperature : int - The power of the general unit in the 'temperature' dimension. + quantity : int + The power of the general unit in the 'quantity' dimension. """ return self._dimensions[5] - def time(self): + def angle(self): """ - Return the power of this general unit in the 'time' dimension. + Return the power of this general unit in the 'angle' dimension. Returns ------- - time : int - The power of the general unit in the 'time' dimension. + angle : int + The power of the general unit in the 'angle' dimension. """ return self._dimensions[6] diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_length.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_length.py index 5eb10fb07..67f163af8 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_length.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_length.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -87,9 +87,8 @@ class Length(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "ANGSTROM" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 0, 1, 0, 0, 0, 0) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ @@ -195,29 +194,6 @@ def __rmul__(self, other): # Multiplication is commutative: a*b = b*a return self.__mul__(other) - def __pow__(self, other): - """Power operator.""" - - if not isinstance(other, int): - raise ValueError("We can only raise to the power of integer values.") - - # No change. - if other == 1: - return self - - # Area. - if other == 2: - mag = self.angstroms().value() ** 2 - return _Area(mag, "A2") - - # Volume. - if other == 3: - mag = self.angstroms().value() ** 3 - return _Volume(mag, "A3") - - else: - return super().__pow__(other) - def meters(self): """ Return the length in meters. @@ -362,7 +338,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. @@ -376,13 +353,13 @@ def _validate_unit(self, unit): unit = "ANGS" + unit[3:] # Check that the unit is supported. - if unit in self._supported_units: + if unit in cls._supported_units: return unit - elif unit in self._abbreviations: - return self._abbreviations[unit] + elif unit in cls._abbreviations: + return cls._abbreviations[unit] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_pressure.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_pressure.py index 699d9d5f2..fbe6da782 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_pressure.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_pressure.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -55,9 +55,8 @@ class Pressure(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "ATMOSPHERE" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 0, -1, 1, 0, 0, -2) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ @@ -177,7 +176,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. @@ -196,11 +196,11 @@ def _validate_unit(self, unit): unit = unit.replace("S", "") # Check that the unit is supported. - if unit in self._abbreviations: - return self._abbreviations[unit] + if unit in cls._abbreviations: + return cls._abbreviations[unit] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_temperature.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_temperature.py index d11d70f0c..a7010dd7f 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_temperature.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_temperature.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -60,9 +60,8 @@ class Temperature(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "KELVIN" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 0, 0, 0, 0, 1, 0) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ @@ -392,7 +391,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. @@ -405,13 +405,16 @@ def _validate_unit(self, unit): unit = unit.replace("DEG", "") # Check that the unit is supported. - if unit in self._supported_units: + if unit in cls._supported_units: return unit - elif unit in self._abbreviations: - return self._abbreviations[unit] + elif unit in cls._abbreviations: + return cls._abbreviations[unit] + elif len(unit) == 0: + raise ValueError(f"Unit is not given. You must supply the unit.") else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Unsupported unit '%s'. Supported units are: '%s'" + % (unit, list(cls._supported_units.keys())) ) def _to_sire_unit(self): @@ -441,13 +444,13 @@ def _from_sire_unit(cls, sire_unit): if isinstance(sire_unit, _SireUnits.GeneralUnit): # Create a mask for the dimensions of the object. dimensions = ( - sire_unit.ANGLE(), - sire_unit.CHARGE(), - sire_unit.LENGTH(), sire_unit.MASS(), - sire_unit.QUANTITY(), - sire_unit.TEMPERATURE(), + sire_unit.LENGTH(), sire_unit.TIME(), + sire_unit.CHARGE(), + sire_unit.TEMPERATURE(), + sire_unit.QUANTITY(), + sire_unit.ANGLE(), ) # Make sure the dimensions match. @@ -470,7 +473,7 @@ def _from_sire_unit(cls, sire_unit): else: raise TypeError( "'sire_unit' must be of type 'sire.units.GeneralUnit', " - "'Sire.Units.Celsius', or 'sire.units.Fahrenheit'" + "'sire.units.Celsius', or 'sire.units.Fahrenheit'" ) @staticmethod diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_time.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_time.py index 19fc10401..0f8dda977 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_time.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_time.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -96,9 +96,8 @@ class Time(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "NANOSECOND" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 0, 0, 0, 0, 0, 1) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ @@ -337,24 +336,25 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. unit = unit.replace(" ", "").upper() # Check that the unit is supported. - if unit in self._supported_units: + if unit in cls._supported_units: return unit - elif unit[:-1] in self._supported_units: + elif unit[:-1] in cls._supported_units: return unit[:-1] - elif unit in self._abbreviations: - return self._abbreviations[unit] - elif unit[:-1] in self._abbreviations: - return self._abbreviations[unit[:-1]] + elif unit in cls._abbreviations: + return cls._abbreviations[unit] + elif unit[:-1] in cls._abbreviations: + return cls._abbreviations[unit[:-1]] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_type.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_type.py index f01635631..75d7b7e0c 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_type.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_type.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -103,7 +103,7 @@ def __init__(self, *args): self._value = temp._value self._unit = temp._unit - # The user has passed a string representation of the temperature. + # The user has passed a string representation of the type. elif isinstance(args[0], str): # Convert the string to an object of this type. obj = self._from_string(args[0]) @@ -168,12 +168,22 @@ def __add__(self, other): temp = self._from_string(other) return self + temp + # Addition of a zero-valued integer or float. + elif isinstance(other, (int, float)) and other == 0: + return self + else: raise TypeError( "unsupported operand type(s) for +: '%s' and '%s'" % (self.__class__.__qualname__, other.__class__.__qualname__) ) + def __radd__(self, other): + """Addition operator.""" + + # Addition is commutative: a+b = b+a + return self.__add__(other) + def __sub__(self, other): """Subtraction operator.""" @@ -185,22 +195,32 @@ def __sub__(self, other): # Return a new object of the same type with the original unit. return self._to_default_unit(val)._convert_to(self._unit) - # Addition of a different type with the same dimensions. + # Subtraction of a different type with the same dimensions. elif isinstance(other, Type) and self._dimensions == other.dimensions: # Negate other and add. return -other + self - # Addition of a string. + # Subtraction of a string. elif isinstance(other, str): temp = self._from_string(other) return self - temp + # Subtraction of a zero-valued integer or float. + elif isinstance(other, (int, float)) and other == 0: + return self + else: raise TypeError( "unsupported operand type(s) for -: '%s' and '%s'" % (self.__class__.__qualname__, other.__class__.__qualname__) ) + def __rsub__(self, other): + """Subtraction operator.""" + + # Subtraction is not commutative: a-b != b-a + return -self.__sub__(other) + def __mul__(self, other): """Multiplication operator.""" @@ -244,19 +264,9 @@ def __rmul__(self, other): def __pow__(self, other): """Power operator.""" - if not isinstance(other, int): - raise ValueError("We can only raise to the power of integer values.") - from ._general_unit import GeneralUnit as _GeneralUnit - default_unit = self._to_default_unit() - mag = default_unit.value() ** other - unit = default_unit.unit().lower() - pow_to_mul = "*".join(abs(other) * [unit]) - if other > 0: - return _GeneralUnit(f"{mag}*{pow_to_mul}") - else: - return _GeneralUnit(f"{mag}/({pow_to_mul})") + return _GeneralUnit(self._to_sire_unit(), no_cast=True) ** other def __truediv__(self, other): """Division operator.""" @@ -486,99 +496,99 @@ def dimensions(cls): containing the power in each dimension. Returns : (int, int, int, int, int, int) - The power in each dimension: 'angle', 'charge', 'length', - 'mass', 'quantity', 'temperature', and 'time'. + The power in each dimension: 'mass', 'length', 'temperature', + 'charge', 'time', 'quantity', and 'angle'. """ return cls._dimensions @classmethod - def angle(cls): + def mass(cls): """ - Return the power in the 'angle' dimension. + Return the power in the 'mass' dimension. Returns ------- - angle : int - The power in the 'angle' dimension. + mass : int + The power in the 'mass' dimension. """ return cls._dimensions[0] @classmethod - def charge(cls): + def length(cls): """ - Return the power in the 'charge' dimension. + Return the power in the 'length' dimension. Returns ------- - charge : int - The power in the 'charge' dimension. + length : int + The power in the 'length' dimension. """ return cls._dimensions[1] @classmethod - def length(cls): + def time(cls): """ - Return the power in the 'length' dimension. + Return the power in the 'time' dimension. Returns ------- - length : int - The power in the 'length' dimension. + time : int + The power the 'time' dimension. """ return cls._dimensions[2] @classmethod - def mass(cls): + def charge(cls): """ - Return the power in the 'mass' dimension. + Return the power in the 'charge' dimension. Returns ------- - mass : int - The power in the 'mass' dimension. + charge : int + The power in the 'charge' dimension. """ return cls._dimensions[3] @classmethod - def quantity(cls): + def temperature(cls): """ - Return the power in the 'quantity' dimension. + Return the power in the 'temperature' dimension. Returns ------- - quantity : int - The power in the 'quantity' dimension. + temperature : int + The power in the 'temperature' dimension. """ return cls._dimensions[4] @classmethod - def temperature(cls): + def quantity(cls): """ - Return the power in the 'temperature' dimension. + Return the power in the 'quantity' dimension. Returns ------- - temperature : int - The power in the 'temperature' dimension. + quantity : int + The power in the 'quantity' dimension. """ return cls._dimensions[5] @classmethod - def time(cls): + def angle(cls): """ - Return the power in the 'time' dimension. + Return the power in the 'angle' dimension. Returns ------- - time : int - The power the 'time' dimension. + angle : int + The power in the 'angle' dimension. """ return cls._dimensions[6] @@ -662,15 +672,7 @@ def _from_sire_unit(cls, sire_unit): raise TypeError("'sire_unit' must be of type 'sire.units.GeneralUnit'") # Create a mask for the dimensions of the object. - dimensions = ( - sire_unit.ANGLE(), - sire_unit.CHARGE(), - sire_unit.LENGTH(), - sire_unit.MASS(), - sire_unit.QUANTITY(), - sire_unit.TEMPERATURE(), - sire_unit.TIME(), - ) + dimensions = tuple(sire_unit.dimensions()) # Make sure that this isn't zero. if hasattr(sire_unit, "is_zero"): diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_volume.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_volume.py index 4b255e01a..4dad85642 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_volume.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_volume.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -72,9 +72,8 @@ class Volume(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "ANGSTROM3" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 0, 3, 0, 0, 0, 0) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ @@ -287,7 +286,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. @@ -317,13 +317,13 @@ def _validate_unit(self, unit): unit = unit[0:index] + unit[index + 1 :] + "3" # Check that the unit is supported. - if unit in self._supported_units: + if unit in cls._supported_units: return unit - elif unit in self._abbreviations: - return self._abbreviations[unit] + elif unit in cls._abbreviations: + return cls._abbreviations[unit] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecule.py b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecule.py index 7bebe86ee..6de6095dd 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecule.py +++ b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecule.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -790,9 +790,8 @@ def makeCompatibleWith( # Have we matched all of the atoms? if len(matches) < num_atoms0: - # Atom names might have changed. Try to match by residue index - # and coordinates. - matcher = _SireMol.ResIdxAtomCoordMatcher() + # Atom names or order might have changed. Try to match by coordinates. + matcher = _SireMol.AtomCoordMatcher() matches = matcher.match(mol0, mol1) # We need to rename the atoms. @@ -992,9 +991,6 @@ def makeCompatibleWith( # Tally counter for the total number of matches. num_matches = 0 - # Initialise the offset. - offset = 0 - # Get the molecule numbers in the system. mol_nums = mol1.molNums() @@ -1004,16 +1000,13 @@ def makeCompatibleWith( mol = mol1[num] # Initialise the matcher. - matcher = _SireMol.ResIdxAtomCoordMatcher(_SireMol.ResIdx(offset)) + matcher = _SireMol.AtomCoordMatcher() # Get the matches for this molecule and append to the list. match = matcher.match(mol0, mol) matches.append(match) num_matches += len(match) - # Increment the offset. - offset += mol.nResidues() - # Have we matched all of the atoms? if num_matches < num_atoms0: raise _IncompatibleError("Failed to match all atoms!") diff --git a/python/BioSimSpace/Types/_angle.py b/python/BioSimSpace/Types/_angle.py index 759a71724..cef19c862 100644 --- a/python/BioSimSpace/Types/_angle.py +++ b/python/BioSimSpace/Types/_angle.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -52,9 +52,8 @@ class Angle(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "RADIAN" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (1, 0, 0, 0, 0, 0, 0) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ @@ -188,7 +187,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. @@ -210,13 +210,13 @@ def _validate_unit(self, unit): unit = unit.replace("AD", "") # Check that the unit is supported. - if unit in self._supported_units: + if unit in cls._supported_units: return unit - elif unit in self._abbreviations: - return self._abbreviations[unit] + elif unit in cls._abbreviations: + return cls._abbreviations[unit] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Types/_area.py b/python/BioSimSpace/Types/_area.py index ec8484cca..2763618a9 100644 --- a/python/BioSimSpace/Types/_area.py +++ b/python/BioSimSpace/Types/_area.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -72,9 +72,8 @@ class Area(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "ANGSTROM2" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 0, 2, 0, 0, 0, 0) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ @@ -330,7 +329,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit is supported.""" # Strip whitespace and convert to upper case. @@ -360,13 +360,13 @@ def _validate_unit(self, unit): unit = unit[0:index] + unit[index + 1 :] + "2" # Check that the unit is supported. - if unit in self._supported_units: + if unit in cls._supported_units: return unit - elif unit in self._abbreviations: - return self._abbreviations[unit] + elif unit in cls._abbreviations: + return cls._abbreviations[unit] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Types/_charge.py b/python/BioSimSpace/Types/_charge.py index 7717f481e..a65d54cd3 100644 --- a/python/BioSimSpace/Types/_charge.py +++ b/python/BioSimSpace/Types/_charge.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -58,9 +58,8 @@ class Charge(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "ELECTRON CHARGE" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 1, 0, 0, 0, 0, 0) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ @@ -182,7 +181,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. @@ -213,11 +213,11 @@ def _validate_unit(self, unit): unit = unit.replace("COUL", "C") # Check that the unit is supported. - if unit in self._abbreviations: - return self._abbreviations[unit] + if unit in cls._abbreviations: + return cls._abbreviations[unit] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Types/_energy.py b/python/BioSimSpace/Types/_energy.py index bb293a17a..af9aa2894 100644 --- a/python/BioSimSpace/Types/_energy.py +++ b/python/BioSimSpace/Types/_energy.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -68,9 +68,8 @@ class Energy(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "KILO CALORIES PER MOL" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 0, 2, 1, -1, 0, -2) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ @@ -213,7 +212,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. @@ -235,11 +235,11 @@ def _validate_unit(self, unit): unit = unit.replace("JOULES", "J") # Check that the unit is supported. - if unit in self._abbreviations: - return self._abbreviations[unit] + if unit in cls._abbreviations: + return cls._abbreviations[unit] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Types/_general_unit.py b/python/BioSimSpace/Types/_general_unit.py index deca60800..7097e616e 100644 --- a/python/BioSimSpace/Types/_general_unit.py +++ b/python/BioSimSpace/Types/_general_unit.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -38,16 +38,16 @@ class GeneralUnit(_Type): """A general unit type.""" _dimension_chars = [ - "A", # Angle - "C", # Charge - "L", # Length "M", # Mass - "Q", # Quantity + "L", # Length + "T", # Time + "C", # Charge "t", # Temperature - "T", # Tme + "Q", # Quantity + "A", # Angle ] - def __new__(cls, *args): + def __new__(cls, *args, no_cast=False): """ Constructor. @@ -65,6 +65,9 @@ def __new__(cls, *args): string : str A string representation of the unit type. + + no_cast: bool + Whether to disable casting to a specific type. """ # This operator may be called when unpickling an object. Catch empty @@ -96,7 +99,7 @@ def __new__(cls, *args): if isinstance(_args[0], _GeneralUnit): general_unit = _args[0] - # The user has passed a string representation of the temperature. + # The user has passed a string representation of the type. elif isinstance(_args[0], str): # Extract the string. string = _args[0] @@ -128,15 +131,7 @@ def __new__(cls, *args): general_unit = value * general_unit # Store the dimension mask. - dimensions = ( - general_unit.ANGLE(), - general_unit.CHARGE(), - general_unit.LENGTH(), - general_unit.MASS(), - general_unit.QUANTITY(), - general_unit.TEMPERATURE(), - general_unit.TIME(), - ) + dimensions = tuple(general_unit.dimensions()) # This is a dimensionless quantity, return the value as a float. if all(x == 0 for x in dimensions): @@ -144,13 +139,13 @@ def __new__(cls, *args): # Check to see if the dimensions correspond to a supported type. # If so, return an object of that type. - if dimensions in _base_dimensions: + if not no_cast and dimensions in _base_dimensions: return _base_dimensions[dimensions](general_unit) # Otherwise, call __init__() else: return super(GeneralUnit, cls).__new__(cls) - def __init__(self, *args): + def __init__(self, *args, no_cast=False): """ Constructor. @@ -168,6 +163,9 @@ def __init__(self, *args): string : str A string representation of the unit type. + + no_cast: bool + Whether to disable casting to a specific type. """ value = 1 @@ -194,7 +192,7 @@ def __init__(self, *args): if isinstance(_args[0], _GeneralUnit): general_unit = _args[0] - # The user has passed a string representation of the temperature. + # The user has passed a string representation of the type. elif isinstance(_args[0], str): # Extract the string. string = _args[0] @@ -222,15 +220,7 @@ def __init__(self, *args): self._value = self._sire_unit.value() # Store the dimension mask. - self._dimensions = ( - general_unit.ANGLE(), - general_unit.CHARGE(), - general_unit.LENGTH(), - general_unit.MASS(), - general_unit.QUANTITY(), - general_unit.TEMPERATURE(), - general_unit.TIME(), - ) + self._dimensions = tuple(general_unit.dimensions()) # Create the unit string. self._unit = "" @@ -271,12 +261,22 @@ def __add__(self, other): temp = self._from_string(other) return self + temp + # Addition of a zero-valued integer or float. + elif isinstance(other, (int, float)) and other == 0: + return self + else: raise TypeError( "unsupported operand type(s) for +: '%s' and '%s'" % (self.__class__.__qualname__, other.__class__.__qualname__) ) + def __radd__(self, other): + """Addition operator.""" + + # Addition is commutative: a+b = b+a + return self.__add__(other) + def __sub__(self, other): """Subtraction operator.""" @@ -285,17 +285,27 @@ def __sub__(self, other): temp = self._sire_unit - other._to_sire_unit() return GeneralUnit(temp) - # Addition of a string. + # Subtraction of a string. elif isinstance(other, str): temp = self._from_string(other) return self - temp + # Subtraction of a zero-valued integer or float. + elif isinstance(other, (int, float)) and other == 0: + return self + else: raise TypeError( "unsupported operand type(s) for -: '%s' and '%s'" % (self.__class__.__qualname__, other.__class__.__qualname__) ) + def __rsub__(self, other): + """Subtraction operator.""" + + # Subtraction is not commutative: a-b != b-a + return -self.__sub__(other) + def __mul__(self, other): """Multiplication operator.""" @@ -312,16 +322,8 @@ def __mul__(self, other): # Multipy the Sire unit objects. temp = self._sire_unit * other._to_sire_unit() - # Create the dimension mask. - dimensions = ( - temp.ANGLE(), - temp.CHARGE(), - temp.LENGTH(), - temp.MASS(), - temp.QUANTITY(), - temp.TEMPERATURE(), - temp.TIME(), - ) + # Get the dimension mask. + dimensions = temp.dimensions() # Return as an existing type if the dimensions match. try: @@ -432,7 +434,7 @@ def __rtruediv__(self, other): def __pow__(self, other): """Power operator.""" - if type(other) is not int: + if not isinstance(other, (int, float)): raise TypeError( "unsupported operand type(s) for ^: '%s' and '%s'" % (self.__class__.__qualname__, other.__class__.__qualname__) @@ -441,15 +443,29 @@ def __pow__(self, other): if other == 0: return GeneralUnit(self._sire_unit / self._sire_unit) - # Multiply the Sire GeneralUnit 'other' times. - temp = self._sire_unit - for x in range(0, abs(other) - 1): - temp = temp * self._sire_unit + # Convert to float. + other = float(other) - if other > 0: - return GeneralUnit(temp) - else: - return GeneralUnit(1 / temp) + # Get the existing unit dimensions. + dims = self.dimensions() + + # Compute the new dimensions, rounding floats to 16 decimal places. + new_dims = [round(dim * other, 16) for dim in dims] + + # Make sure the new dimensions are integers. + if not all(dim.is_integer() for dim in new_dims): + raise ValueError( + "The exponent must be a factor of all the unit dimensions." + ) + + # Convert to integers. + new_dims = [int(dim) for dim in new_dims] + + # Compute the new value. + value = self.value() ** other + + # Return a new GeneralUnit object. + return GeneralUnit(_GeneralUnit(value, new_dims)) def __lt__(self, other): """Less than operator.""" @@ -606,87 +622,87 @@ def dimensions(self): """ return self._dimensions - def angle(self): + def mass(self): """ - Return the power of this general unit in the 'angle' dimension. + Return the power of this general unit in the 'mass' dimension. Returns ------- - angle : int - The power of the general unit in the 'angle' dimension. + mass : int + The power of the general unit in the 'mass' dimension. """ return self._dimensions[0] - def charge(self): + def length(self): """ - Return the power of this general unit in the 'charge' dimension. + Return the power of this general unit in the 'length' dimension. Returns ------- - charge : int - The power of the general unit in the 'charge' dimension. + length : int + The power of the general unit in the 'length' dimension. """ return self._dimensions[1] - def length(self): + def time(self): """ - Return the power of this general unit in the 'length' dimension. + Return the power of this general unit in the 'time' dimension. Returns ------- - length : int - The power of the general unit in the 'length' dimension. + time : int + The power of the general unit in the 'time' dimension. """ return self._dimensions[2] - def mass(self): + def charge(self): """ - Return the power of this general unit in the 'mass' dimension. + Return the power of this general unit in the 'charge' dimension. Returns ------- - mass : int - The power of the general unit in the 'mass' dimension. + charge : int + The power of the general unit in the 'charge' dimension. """ return self._dimensions[3] - def quantity(self): + def temperature(self): """ - Return the power of this general unit in the 'quantity' dimension. + Return the power of this general unit in the 'temperature' dimension. Returns ------- - quantity : int - The power of the general unit in the 'quantity' dimension. + temperature : int + The power of the general unit in the 'temperature' dimension. """ return self._dimensions[4] - def temperature(self): + def quantity(self): """ - Return the power of this general unit in the 'temperature' dimension. + Return the power of this general unit in the 'quantity' dimension. Returns ------- - temperature : int - The power of the general unit in the 'temperature' dimension. + quantity : int + The power of the general unit in the 'quantity' dimension. """ return self._dimensions[5] - def time(self): + def angle(self): """ - Return the power of this general unit in the 'time' dimension. + Return the power of this general unit in the 'angle' dimension. Returns ------- - time : int - The power of the general unit in the 'time' dimension. + angle : int + The power of the general unit in the 'angle' dimension. """ return self._dimensions[6] diff --git a/python/BioSimSpace/Types/_length.py b/python/BioSimSpace/Types/_length.py index 5eb10fb07..67f163af8 100644 --- a/python/BioSimSpace/Types/_length.py +++ b/python/BioSimSpace/Types/_length.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -87,9 +87,8 @@ class Length(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "ANGSTROM" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 0, 1, 0, 0, 0, 0) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ @@ -195,29 +194,6 @@ def __rmul__(self, other): # Multiplication is commutative: a*b = b*a return self.__mul__(other) - def __pow__(self, other): - """Power operator.""" - - if not isinstance(other, int): - raise ValueError("We can only raise to the power of integer values.") - - # No change. - if other == 1: - return self - - # Area. - if other == 2: - mag = self.angstroms().value() ** 2 - return _Area(mag, "A2") - - # Volume. - if other == 3: - mag = self.angstroms().value() ** 3 - return _Volume(mag, "A3") - - else: - return super().__pow__(other) - def meters(self): """ Return the length in meters. @@ -362,7 +338,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. @@ -376,13 +353,13 @@ def _validate_unit(self, unit): unit = "ANGS" + unit[3:] # Check that the unit is supported. - if unit in self._supported_units: + if unit in cls._supported_units: return unit - elif unit in self._abbreviations: - return self._abbreviations[unit] + elif unit in cls._abbreviations: + return cls._abbreviations[unit] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Types/_pressure.py b/python/BioSimSpace/Types/_pressure.py index 699d9d5f2..fbe6da782 100644 --- a/python/BioSimSpace/Types/_pressure.py +++ b/python/BioSimSpace/Types/_pressure.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -55,9 +55,8 @@ class Pressure(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "ATMOSPHERE" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 0, -1, 1, 0, 0, -2) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ @@ -177,7 +176,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. @@ -196,11 +196,11 @@ def _validate_unit(self, unit): unit = unit.replace("S", "") # Check that the unit is supported. - if unit in self._abbreviations: - return self._abbreviations[unit] + if unit in cls._abbreviations: + return cls._abbreviations[unit] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Types/_temperature.py b/python/BioSimSpace/Types/_temperature.py index f97d2b956..a7010dd7f 100644 --- a/python/BioSimSpace/Types/_temperature.py +++ b/python/BioSimSpace/Types/_temperature.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -60,9 +60,8 @@ class Temperature(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "KELVIN" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 0, 0, 0, 0, 1, 0) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ @@ -392,7 +391,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. @@ -405,16 +405,16 @@ def _validate_unit(self, unit): unit = unit.replace("DEG", "") # Check that the unit is supported. - if unit in self._supported_units: + if unit in cls._supported_units: return unit - elif unit in self._abbreviations: - return self._abbreviations[unit] + elif unit in cls._abbreviations: + return cls._abbreviations[unit] elif len(unit) == 0: raise ValueError(f"Unit is not given. You must supply the unit.") else: raise ValueError( "Unsupported unit '%s'. Supported units are: '%s'" - % (unit, list(self._supported_units.keys())) + % (unit, list(cls._supported_units.keys())) ) def _to_sire_unit(self): @@ -444,13 +444,13 @@ def _from_sire_unit(cls, sire_unit): if isinstance(sire_unit, _SireUnits.GeneralUnit): # Create a mask for the dimensions of the object. dimensions = ( - sire_unit.ANGLE(), - sire_unit.CHARGE(), - sire_unit.LENGTH(), sire_unit.MASS(), - sire_unit.QUANTITY(), - sire_unit.TEMPERATURE(), + sire_unit.LENGTH(), sire_unit.TIME(), + sire_unit.CHARGE(), + sire_unit.TEMPERATURE(), + sire_unit.QUANTITY(), + sire_unit.ANGLE(), ) # Make sure the dimensions match. diff --git a/python/BioSimSpace/Types/_time.py b/python/BioSimSpace/Types/_time.py index 19fc10401..0f8dda977 100644 --- a/python/BioSimSpace/Types/_time.py +++ b/python/BioSimSpace/Types/_time.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -96,9 +96,8 @@ class Time(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "NANOSECOND" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 0, 0, 0, 0, 0, 1) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ @@ -337,24 +336,25 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. unit = unit.replace(" ", "").upper() # Check that the unit is supported. - if unit in self._supported_units: + if unit in cls._supported_units: return unit - elif unit[:-1] in self._supported_units: + elif unit[:-1] in cls._supported_units: return unit[:-1] - elif unit in self._abbreviations: - return self._abbreviations[unit] - elif unit[:-1] in self._abbreviations: - return self._abbreviations[unit[:-1]] + elif unit in cls._abbreviations: + return cls._abbreviations[unit] + elif unit[:-1] in cls._abbreviations: + return cls._abbreviations[unit[:-1]] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Types/_type.py b/python/BioSimSpace/Types/_type.py index f01635631..75d7b7e0c 100644 --- a/python/BioSimSpace/Types/_type.py +++ b/python/BioSimSpace/Types/_type.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -103,7 +103,7 @@ def __init__(self, *args): self._value = temp._value self._unit = temp._unit - # The user has passed a string representation of the temperature. + # The user has passed a string representation of the type. elif isinstance(args[0], str): # Convert the string to an object of this type. obj = self._from_string(args[0]) @@ -168,12 +168,22 @@ def __add__(self, other): temp = self._from_string(other) return self + temp + # Addition of a zero-valued integer or float. + elif isinstance(other, (int, float)) and other == 0: + return self + else: raise TypeError( "unsupported operand type(s) for +: '%s' and '%s'" % (self.__class__.__qualname__, other.__class__.__qualname__) ) + def __radd__(self, other): + """Addition operator.""" + + # Addition is commutative: a+b = b+a + return self.__add__(other) + def __sub__(self, other): """Subtraction operator.""" @@ -185,22 +195,32 @@ def __sub__(self, other): # Return a new object of the same type with the original unit. return self._to_default_unit(val)._convert_to(self._unit) - # Addition of a different type with the same dimensions. + # Subtraction of a different type with the same dimensions. elif isinstance(other, Type) and self._dimensions == other.dimensions: # Negate other and add. return -other + self - # Addition of a string. + # Subtraction of a string. elif isinstance(other, str): temp = self._from_string(other) return self - temp + # Subtraction of a zero-valued integer or float. + elif isinstance(other, (int, float)) and other == 0: + return self + else: raise TypeError( "unsupported operand type(s) for -: '%s' and '%s'" % (self.__class__.__qualname__, other.__class__.__qualname__) ) + def __rsub__(self, other): + """Subtraction operator.""" + + # Subtraction is not commutative: a-b != b-a + return -self.__sub__(other) + def __mul__(self, other): """Multiplication operator.""" @@ -244,19 +264,9 @@ def __rmul__(self, other): def __pow__(self, other): """Power operator.""" - if not isinstance(other, int): - raise ValueError("We can only raise to the power of integer values.") - from ._general_unit import GeneralUnit as _GeneralUnit - default_unit = self._to_default_unit() - mag = default_unit.value() ** other - unit = default_unit.unit().lower() - pow_to_mul = "*".join(abs(other) * [unit]) - if other > 0: - return _GeneralUnit(f"{mag}*{pow_to_mul}") - else: - return _GeneralUnit(f"{mag}/({pow_to_mul})") + return _GeneralUnit(self._to_sire_unit(), no_cast=True) ** other def __truediv__(self, other): """Division operator.""" @@ -486,99 +496,99 @@ def dimensions(cls): containing the power in each dimension. Returns : (int, int, int, int, int, int) - The power in each dimension: 'angle', 'charge', 'length', - 'mass', 'quantity', 'temperature', and 'time'. + The power in each dimension: 'mass', 'length', 'temperature', + 'charge', 'time', 'quantity', and 'angle'. """ return cls._dimensions @classmethod - def angle(cls): + def mass(cls): """ - Return the power in the 'angle' dimension. + Return the power in the 'mass' dimension. Returns ------- - angle : int - The power in the 'angle' dimension. + mass : int + The power in the 'mass' dimension. """ return cls._dimensions[0] @classmethod - def charge(cls): + def length(cls): """ - Return the power in the 'charge' dimension. + Return the power in the 'length' dimension. Returns ------- - charge : int - The power in the 'charge' dimension. + length : int + The power in the 'length' dimension. """ return cls._dimensions[1] @classmethod - def length(cls): + def time(cls): """ - Return the power in the 'length' dimension. + Return the power in the 'time' dimension. Returns ------- - length : int - The power in the 'length' dimension. + time : int + The power the 'time' dimension. """ return cls._dimensions[2] @classmethod - def mass(cls): + def charge(cls): """ - Return the power in the 'mass' dimension. + Return the power in the 'charge' dimension. Returns ------- - mass : int - The power in the 'mass' dimension. + charge : int + The power in the 'charge' dimension. """ return cls._dimensions[3] @classmethod - def quantity(cls): + def temperature(cls): """ - Return the power in the 'quantity' dimension. + Return the power in the 'temperature' dimension. Returns ------- - quantity : int - The power in the 'quantity' dimension. + temperature : int + The power in the 'temperature' dimension. """ return cls._dimensions[4] @classmethod - def temperature(cls): + def quantity(cls): """ - Return the power in the 'temperature' dimension. + Return the power in the 'quantity' dimension. Returns ------- - temperature : int - The power in the 'temperature' dimension. + quantity : int + The power in the 'quantity' dimension. """ return cls._dimensions[5] @classmethod - def time(cls): + def angle(cls): """ - Return the power in the 'time' dimension. + Return the power in the 'angle' dimension. Returns ------- - time : int - The power the 'time' dimension. + angle : int + The power in the 'angle' dimension. """ return cls._dimensions[6] @@ -662,15 +672,7 @@ def _from_sire_unit(cls, sire_unit): raise TypeError("'sire_unit' must be of type 'sire.units.GeneralUnit'") # Create a mask for the dimensions of the object. - dimensions = ( - sire_unit.ANGLE(), - sire_unit.CHARGE(), - sire_unit.LENGTH(), - sire_unit.MASS(), - sire_unit.QUANTITY(), - sire_unit.TEMPERATURE(), - sire_unit.TIME(), - ) + dimensions = tuple(sire_unit.dimensions()) # Make sure that this isn't zero. if hasattr(sire_unit, "is_zero"): diff --git a/python/BioSimSpace/Types/_volume.py b/python/BioSimSpace/Types/_volume.py index 4b255e01a..4dad85642 100644 --- a/python/BioSimSpace/Types/_volume.py +++ b/python/BioSimSpace/Types/_volume.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -72,9 +72,8 @@ class Volume(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "ANGSTROM3" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 0, 3, 0, 0, 0, 0) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ @@ -287,7 +286,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. @@ -317,13 +317,13 @@ def _validate_unit(self, unit): unit = unit[0:index] + unit[index + 1 :] + "3" # Check that the unit is supported. - if unit in self._supported_units: + if unit in cls._supported_units: return unit - elif unit in self._abbreviations: - return self._abbreviations[unit] + elif unit in cls._abbreviations: + return cls._abbreviations[unit] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/_Config/_amber.py b/python/BioSimSpace/_Config/_amber.py index ddd51ed41..2a93e52a8 100644 --- a/python/BioSimSpace/_Config/_amber.py +++ b/python/BioSimSpace/_Config/_amber.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -224,9 +224,9 @@ def createConfig( ] restraint_mask = "@" + ",".join(restraint_atom_names) elif restraint == "heavy": - restraint_mask = "!:WAT & !@H=" + restraint_mask = "!:WAT & !@%NA,CL & !@H=" elif restraint == "all": - restraint_mask = "!:WAT" + restraint_mask = "!:WAT & !@%NA,CL" # We can't do anything about a custom restraint, since we don't # know anything about the atoms. diff --git a/python/BioSimSpace/_SireWrappers/_molecule.py b/python/BioSimSpace/_SireWrappers/_molecule.py index 9a3a3d70e..089c88146 100644 --- a/python/BioSimSpace/_SireWrappers/_molecule.py +++ b/python/BioSimSpace/_SireWrappers/_molecule.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -746,9 +746,8 @@ def makeCompatibleWith( # Have we matched all of the atoms? if len(matches) < num_atoms0: - # Atom names might have changed. Try to match by residue index - # and coordinates. - matcher = _SireMol.ResIdxAtomCoordMatcher() + # Atom names or order might have changed. Try to match by coordinates. + matcher = _SireMol.AtomCoordMatcher() matches = matcher.match(mol0, mol1) # We need to rename the atoms. @@ -948,9 +947,6 @@ def makeCompatibleWith( # Tally counter for the total number of matches. num_matches = 0 - # Initialise the offset. - offset = 0 - # Get the molecule numbers in the system. mol_nums = mol1.molNums() @@ -960,16 +956,13 @@ def makeCompatibleWith( mol = mol1[num] # Initialise the matcher. - matcher = _SireMol.ResIdxAtomCoordMatcher(_SireMol.ResIdx(offset)) + matcher = _SireMol.AtomCoordMatcher() # Get the matches for this molecule and append to the list. match = matcher.match(mol0, mol) matches.append(match) num_matches += len(match) - # Increment the offset. - offset += mol.nResidues() - # Have we matched all of the atoms? if num_matches < num_atoms0: raise _IncompatibleError("Failed to match all atoms!") diff --git a/recipes/biosimspace/template.yaml b/recipes/biosimspace/template.yaml index 50b670ded..efcb2c141 100644 --- a/recipes/biosimspace/template.yaml +++ b/recipes/biosimspace/template.yaml @@ -26,18 +26,19 @@ test: - SIRE_DONT_PHONEHOME - SIRE_SILENT_PHONEHOME requires: - - pytest - - black 23 # [linux and x86_64 and py==39] - - pytest-black # [linux and x86_64 and py==39] + - pytest <8 + - black 23 # [linux and x86_64 and py==311] + - pytest-black # [linux and x86_64 and py==311] - ambertools # [linux and x86_64] - gromacs # [linux and x86_64] + - requests imports: - BioSimSpace source_files: - - python/BioSimSpace # [linux and x86_64 and py==39] + - python/BioSimSpace # [linux and x86_64 and py==311] - tests commands: - - pytest -vvv --color=yes --black python/BioSimSpace # [linux and x86_64 and py==39] + - pytest -vvv --color=yes --black python/BioSimSpace # [linux and x86_64 and py==311] - pytest -vvv --color=yes --import-mode=importlib tests about: diff --git a/requirements.txt b/requirements.txt index f7bb52604..79c862907 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ # BioSimSpace runtime requirements. # main -sire~=2023.5.0 +sire~=2023.5.2 # devel #sire==2023.5.0.dev diff --git a/tests/Sandpit/Exscientia/FreeEnergy/test_restraint_search.py b/tests/Sandpit/Exscientia/FreeEnergy/test_restraint_search.py index c7489f8d2..b34496bb0 100644 --- a/tests/Sandpit/Exscientia/FreeEnergy/test_restraint_search.py +++ b/tests/Sandpit/Exscientia/FreeEnergy/test_restraint_search.py @@ -291,7 +291,15 @@ def test_dict_mdr(self, multiple_distance_restraint): assert restr_dict["permanent_distance_restraint"][ "r0" ].value() == pytest.approx(8.9019, abs=1e-4) - assert restr_dict["permanent_distance_restraint"]["kr"].unit() == "M Q-1 T-2" + assert restr_dict["permanent_distance_restraint"]["kr"].dimensions() == ( + 1, + 0, + -2, + 0, + 0, + -1, + 0, + ) assert restr_dict["permanent_distance_restraint"]["kr"].value() == 40.0 assert restr_dict["permanent_distance_restraint"]["r_fb"].unit() == "ANGSTROM" assert restr_dict["permanent_distance_restraint"][ diff --git a/tests/Sandpit/Exscientia/Types/test_general_unit.py b/tests/Sandpit/Exscientia/Types/test_general_unit.py index 3862c2a47..efc3cd4e1 100644 --- a/tests/Sandpit/Exscientia/Types/test_general_unit.py +++ b/tests/Sandpit/Exscientia/Types/test_general_unit.py @@ -3,15 +3,26 @@ import BioSimSpace.Sandpit.Exscientia.Types as Types import BioSimSpace.Sandpit.Exscientia.Units as Units +import sire as sr + @pytest.mark.parametrize( "string, dimensions", [ - ("kilo Cal oriEs per Mole / angstrom **2", (0, 0, 0, 1, -1, 0, -2)), - ("k Cal_per _mOl / nm^2", (0, 0, 0, 1, -1, 0, -2)), - ("kj p eR moles / pico METERs2", (0, 0, 0, 1, -1, 0, -2)), - ("coul oMbs / secs * ATm os phereS", (0, 1, -1, 1, 0, 0, -3)), - ("pm**3 * rads * de grEE", (2, 0, 3, 0, 0, 0, 0)), + ( + "kilo Cal oriEs per Mole / angstrom **2", + tuple(sr.u("kcal_per_mol / angstrom**2").dimensions()), + ), + ("k Cal_per _mOl / nm^2", tuple(sr.u("kcal_per_mol / nm**2").dimensions())), + ( + "kj p eR moles / pico METERs2", + tuple(sr.u("kJ_per_mol / pm**2").dimensions()), + ), + ( + "coul oMbs / secs * ATm os phereS", + tuple(sr.u("coulombs / second / atm").dimensions()), + ), + ("pm**3 * rads * de grEE", tuple(sr.u("pm**3 * rad * degree").dimensions())), ], ) def test_supported_units(string, dimensions): @@ -140,6 +151,61 @@ def test_neg_pow(unit_type): assert d1 == -d0 +def test_frac_pow(): + """Test that unit-based types can be raised to fractional powers.""" + + # Create a base unit type. + unit_type = 2 * Units.Length.angstrom + + # Store the original value and dimensions. + value = unit_type.value() + dimensions = unit_type.dimensions() + + # Square the type. + unit_type = unit_type**2 + + # Assert that we can't take the cube root. + with pytest.raises(ValueError): + unit_type = unit_type ** (1 / 3) + + # Now take the square root. + unit_type = unit_type ** (1 / 2) + + # The value should be the same. + assert unit_type.value() == value + + # The dimensions should be the same. + assert unit_type.dimensions() == dimensions + + # Cube the type. + unit_type = unit_type**3 + + # Assert that we can't take the square root. + with pytest.raises(ValueError): + unit_type = unit_type ** (1 / 2) + + # Now take the cube root. + unit_type = unit_type ** (1 / 3) + + # The value should be the same. + assert unit_type.value() == value + + # The dimensions should be the same. + assert unit_type.dimensions() == dimensions + + # Square the type again. + unit_type = unit_type**2 + + # Now take the negative square root. + unit_type = unit_type ** (-1 / 2) + + # The value should be inverted. + assert unit_type.value() == 1 / value + + # The dimensions should be negated. + assert unit_type.dimensions() == tuple(-d for d in dimensions) + + @pytest.mark.parametrize( "string", [ diff --git a/tests/Types/test_general_unit.py b/tests/Types/test_general_unit.py index d97acaf36..ec630d060 100644 --- a/tests/Types/test_general_unit.py +++ b/tests/Types/test_general_unit.py @@ -3,15 +3,26 @@ import BioSimSpace.Types as Types import BioSimSpace.Units as Units +import sire as sr + @pytest.mark.parametrize( "string, dimensions", [ - ("kilo Cal oriEs per Mole / angstrom **2", (0, 0, 0, 1, -1, 0, -2)), - ("k Cal_per _mOl / nm^2", (0, 0, 0, 1, -1, 0, -2)), - ("kj p eR moles / pico METERs2", (0, 0, 0, 1, -1, 0, -2)), - ("coul oMbs / secs * ATm os phereS", (0, 1, -1, 1, 0, 0, -3)), - ("pm**3 * rads * de grEE", (2, 0, 3, 0, 0, 0, 0)), + ( + "kilo Cal oriEs per Mole / angstrom **2", + tuple(sr.u("kcal_per_mol / angstrom**2").dimensions()), + ), + ("k Cal_per _mOl / nm^2", tuple(sr.u("kcal_per_mol / nm**2").dimensions())), + ( + "kj p eR moles / pico METERs2", + tuple(sr.u("kJ_per_mol / pm**2").dimensions()), + ), + ( + "coul oMbs / secs * ATm os phereS", + tuple(sr.u("coulombs / second / atm").dimensions()), + ), + ("pm**3 * rads * de grEE", tuple(sr.u("pm**3 * rad * degree").dimensions())), ], ) def test_supported_units(string, dimensions): @@ -140,6 +151,61 @@ def test_neg_pow(unit_type): assert d1 == -d0 +def test_frac_pow(): + """Test that unit-based types can be raised to fractional powers.""" + + # Create a base unit type. + unit_type = 2 * Units.Length.angstrom + + # Store the original value and dimensions. + value = unit_type.value() + dimensions = unit_type.dimensions() + + # Square the type. + unit_type = unit_type**2 + + # Assert that we can't take the cube root. + with pytest.raises(ValueError): + unit_type = unit_type ** (1 / 3) + + # Now take the square root. + unit_type = unit_type ** (1 / 2) + + # The value should be the same. + assert unit_type.value() == value + + # The dimensions should be the same. + assert unit_type.dimensions() == dimensions + + # Cube the type. + unit_type = unit_type**3 + + # Assert that we can't take the square root. + with pytest.raises(ValueError): + unit_type = unit_type ** (1 / 2) + + # Now take the cube root. + unit_type = unit_type ** (1 / 3) + + # The value should be the same. + assert unit_type.value() == value + + # The dimensions should be the same. + assert unit_type.dimensions() == dimensions + + # Square the type again. + unit_type = unit_type**2 + + # Now take the negative square root. + unit_type = unit_type ** (-1 / 2) + + # The value should be inverted. + assert unit_type.value() == 1 / value + + # The dimensions should be negated. + assert unit_type.dimensions() == tuple(-d for d in dimensions) + + @pytest.mark.parametrize( "string", [