diff --git a/.gitignore b/.gitignore index 3787d7096..0213a2c78 100644 --- a/.gitignore +++ b/.gitignore @@ -119,6 +119,11 @@ md.log *stderr.txt *stdout.txt +# LAMMPS +log.lammps +out.lmp +tmp.in + # OS .DS_Store .DS_Store? diff --git a/devtools/conda-envs/dev_env.yaml b/devtools/conda-envs/dev_env.yaml index 0ca7d2ab9..f2e3aa34d 100644 --- a/devtools/conda-envs/dev_env.yaml +++ b/devtools/conda-envs/dev_env.yaml @@ -4,7 +4,7 @@ channels: - openeye dependencies: # Core - - python =3.10 + - python =3.11 - pip - numpy - pydantic =2 @@ -12,14 +12,14 @@ dependencies: # OpenFF stack - openff-toolkit ~=0.16 - openff-interchange-base - # smirnoff-plugins =2024 + - smirnoff-plugins =2024 - openff-nagl - openff-nagl-models - ambertools =23 # Optional features - - mbuild =0.17 - - foyer >=0.12.1 - - gmso =0.12 + - mbuild ~=0.17 + - foyer ~=0.12 + - gmso ~=0.12 # Testing - mdtraj - intermol @@ -31,7 +31,7 @@ dependencies: - nbval # de-forcefields # add back after smirnoff-plugins update # Drivers - - gromacs + - gromacs =2024 - lammps >=2023.08.02 - panedr # Typing diff --git a/docs/using/collections.md b/docs/using/collections.md index 8bc142329..229e6752b 100644 --- a/docs/using/collections.md +++ b/docs/using/collections.md @@ -91,13 +91,23 @@ SMIRNOFFImproperTorsionCollection(type='ImproperTorsions', ``` +We can also access a collection by indexing directly into the Interchange itself: + +```pycon +>>> interchange['ImproperTorsions'] # doctest: +NORMALIZE_WHITESPACE,+ELLIPSIS +SMIRNOFFImproperTorsionCollection(type='ImproperTorsions', + expression='k*(1+cos(periodicity*theta-phase))', + key_map={}, + potentials={}) +``` + In the bond collection for example, each pair of bonded atoms maps to one of two potential keys, one for the carbon-carbon bond, and the other for the carbon-hydrogen bonds. It's clear from the SMIRKS codes that atoms 0 and 1 are the carbon atoms, and atoms 2 through 7 are the hydrogens: ```pycon ->>> interchange.collections['Bonds'].key_map # doctest: +NORMALIZE_WHITESPACE,+ELLIPSIS +>>> interchange['Bonds'].key_map # doctest: +NORMALIZE_WHITESPACE,+ELLIPSIS {TopologyKey(atom_indices=(0, 1), ...): PotentialKey(id='[#6X4:1]-[#6X4:2]', ...), TopologyKey(atom_indices=(0, 2), ...): PotentialKey(id='[#6X4:1]-[#1:2]', ...), TopologyKey(atom_indices=(0, 3), ...): PotentialKey(id='[#6X4:1]-[#1:2]', ...), @@ -117,7 +127,7 @@ The bond collection also maps the two potential keys to the appropriate `Potenti Here we can read off the force constant and length: ```pycon ->>> interchange.collections['Bonds'].potentials # doctest: +NORMALIZE_WHITESPACE,+ELLIPSIS +>>> interchange['Bonds'].potentials # doctest: +NORMALIZE_WHITESPACE,+ELLIPSIS {PotentialKey(id='[#6X4:1]-[#6X4:2]', ...): Potential(parameters={'k': , 'length': }, ...), @@ -127,16 +137,21 @@ Here we can read off the force constant and length: ``` -We can even modify a value here, export the new interchange, and see that all of -the bonds have been updated: +Any `TopologyKey` that only specifies atom indices can be accessed by indexing directly into the `Collection` with those atom indices: + +```pycon +>>> interchange['Bonds'][0,1] # doctest: +NORMALIZE_WHITESPACE,+ELLIPSIS +Potential(parameters={'k': , 'length': }, map_key=None) + +``` + +We can even modify a value here, export the new interchange, and see that all of the bonds have been updated: ```pycon >>> from openff.interchange.models import TopologyKey >>> from openff.units import unit >>> # Get the potential from the first C-H bond ->>> top_key = TopologyKey(atom_indices=(0, 2)) ->>> pot_key = interchange.collections['Bonds'].key_map[top_key] ->>> potential = interchange.collections['Bonds'].potentials[pot_key] +>>> potential = interchange['Bonds'][0, 2] >>> # Modify the potential >>> potential.parameters['length'] = 3.1415926 * unit.nanometer >>> # Write out the modified interchange to a GROMACS .top file diff --git a/openff/interchange/_tests/unit_tests/components/test_interchange.py b/openff/interchange/_tests/unit_tests/components/test_interchange.py index c771538ad..6a0c97663 100644 --- a/openff/interchange/_tests/unit_tests/components/test_interchange.py +++ b/openff/interchange/_tests/unit_tests/components/test_interchange.py @@ -48,6 +48,14 @@ def test_getitem(self, sage): with pytest.raises(LookupError, match="Could not find"): out["CMAPs"] + first_bondkey = next(iter(out["Bonds"].key_map)) + idx_a, idx_b = first_bondkey.atom_indices + assert ( + out["Bonds"][idx_a, idx_b] + == out["Bonds"][idx_b, idx_a] + == out["Bonds"].potentials[out["Bonds"].key_map[first_bondkey]] + ) + def test_get_parameters(self, sage): mol = Molecule.from_smiles("CCO") out = Interchange.from_smirnoff(force_field=sage, topology=[mol]) diff --git a/openff/interchange/_tests/unit_tests/interop/openmm/test_virtual_sites.py b/openff/interchange/_tests/unit_tests/interop/openmm/test_virtual_sites.py index cbd69359c..8e121b4c6 100644 --- a/openff/interchange/_tests/unit_tests/interop/openmm/test_virtual_sites.py +++ b/openff/interchange/_tests/unit_tests/interop/openmm/test_virtual_sites.py @@ -330,7 +330,6 @@ def test_tip5p_num_exceptions(self, water, tip5p, combine, n_molecules): # Safeguard against some of the behavior seen in #919 for index in range(num_exceptions): p1, p2, *_ = force.getExceptionParameters(index) - print(p1, p2) if sorted([p1, p2]) == [0, 3]: raise Exception( diff --git a/openff/interchange/_tests/unit_tests/smirnoff/test_virtual_sites.py b/openff/interchange/_tests/unit_tests/smirnoff/test_virtual_sites.py index c5c7111a0..f536f506d 100644 --- a/openff/interchange/_tests/unit_tests/smirnoff/test_virtual_sites.py +++ b/openff/interchange/_tests/unit_tests/smirnoff/test_virtual_sites.py @@ -205,7 +205,7 @@ def generate_v_site_coordinates( (0, 1, 2, 3), ( VirtualSiteMocking.sp2_conformer()[0] - + Quantity( # noqa + + Quantity( numpy.array( [[1.0, numpy.sqrt(2), 1.0], [1.0, -numpy.sqrt(2), -1.0]], ), diff --git a/openff/interchange/_tests/unit_tests/test_models.py b/openff/interchange/_tests/unit_tests/test_models.py index 81f6609f9..617a66cb7 100644 --- a/openff/interchange/_tests/unit_tests/test_models.py +++ b/openff/interchange/_tests/unit_tests/test_models.py @@ -1,4 +1,5 @@ from openff.interchange.models import ( + AngleKey, BondKey, ImproperTorsionKey, PotentialKey, @@ -110,3 +111,90 @@ def test_reprs(): assert "blah" in repr(potential_key) assert "mult 2" in repr(potential_key) assert "bond order 1.111" in repr(potential_key) + + +def test_bondkey_eq_hash(): + """ + When __eq__ is true, the hashes must be equal. + + The converse is not required in Python; hash collisions between unequal + objects are allowed and will be handled according to __eq__, possibly + with a small runtime cost. + """ + + assert BondKey(atom_indices=(1, 3)) == BondKey(atom_indices=(1, 3)) + assert BondKey(atom_indices=(1, 3)) == TopologyKey(atom_indices=(1, 3)) + assert BondKey(atom_indices=(1, 3)) == (1, 3) + assert hash(BondKey(atom_indices=(1, 3))) == hash((1, 3)) + assert BondKey(atom_indices=(1, 3)) != (1, 4) + assert BondKey(atom_indices=(1, 3)) != (3, 1) + assert BondKey(atom_indices=(1, 3), bond_order=None) == (1, 3) + assert hash(BondKey(atom_indices=(1, 3), bond_order=None)) == hash((1, 3)) + assert BondKey(atom_indices=(1, 3), bond_order=None) != ((1, 3), None) + assert BondKey(atom_indices=(1, 3), bond_order=1.5) != (1, 3) + assert BondKey(atom_indices=(1, 3), bond_order=1.5) != ((1, 3), None) + assert BondKey(atom_indices=(1, 3), bond_order=1.5) == ((1, 3), 1.5) + + +def test_anglekey_eq_hash(): + """ + When __eq__ is true, the hashes must be equal. + + The converse is not required in Python; hash collisions between unequal + objects are allowed and will be handled according to __eq__, possibly + with a small runtime cost. + """ + assert AngleKey(atom_indices=(1, 3, 16)) == AngleKey(atom_indices=(1, 3, 16)) + assert AngleKey(atom_indices=(1, 3, 16)) == TopologyKey(atom_indices=(1, 3, 16)) + assert AngleKey(atom_indices=(1, 3, 16)) == (1, 3, 16) + assert hash(AngleKey(atom_indices=(1, 3, 16))) == hash((1, 3, 16)) + assert AngleKey(atom_indices=(1, 3, 16)) != (16, 3, 1) + assert AngleKey(atom_indices=(1, 3, 16)) != (1, 3) + assert AngleKey(atom_indices=(1, 3, 16)) != (1, 3, 15) + + +def test_torsionkey_eq_hash(): + """ + When __eq__ is true, the hashes must be equal. + + The converse is not required in Python; hash collisions between unequal + objects are allowed and will be handled according to __eq__, possibly + with a small runtime cost. + """ + assert ProperTorsionKey(atom_indices=(1, 2, 3, 4)) == ProperTorsionKey( + atom_indices=(1, 2, 3, 4), + ) + assert ProperTorsionKey(atom_indices=(1, 2, 3, 4)) == TopologyKey( + atom_indices=(1, 2, 3, 4), + ) + assert ProperTorsionKey(atom_indices=(1, 2, 3, 4)) == (1, 2, 3, 4) + assert hash(ProperTorsionKey(atom_indices=(1, 2, 3, 4))) == hash((1, 2, 3, 4)) + assert ProperTorsionKey(atom_indices=(1, 2, 3, 4)) != (4, 3, 2, 1) + assert ProperTorsionKey( + atom_indices=(1, 2, 3, 4), + mult=None, + phase=None, + bond_order=None, + ) == (1, 2, 3, 4) + assert ProperTorsionKey(atom_indices=(1, 2, 3, 4), mult=0) != (1, 2, 3, 4) + assert ProperTorsionKey(atom_indices=(1, 2, 3, 4), phase=1.5) != (1, 2, 3, 4) + assert ProperTorsionKey(atom_indices=(1, 2, 3, 4), bond_order=1.5) != (1, 2, 3, 4) + + assert ProperTorsionKey(atom_indices=(1, 2, 3, 4), mult=0) == ( + (1, 2, 3, 4), + 0, + None, + None, + ) + assert ProperTorsionKey(atom_indices=(1, 2, 3, 4), phase=1.5) == ( + (1, 2, 3, 4), + None, + 1.5, + None, + ) + assert ProperTorsionKey(atom_indices=(1, 2, 3, 4), bond_order=1.5) == ( + (1, 2, 3, 4), + None, + None, + 1.5, + ) diff --git a/openff/interchange/components/_packmol.py b/openff/interchange/components/_packmol.py index eafa9798d..6ebf00af3 100644 --- a/openff/interchange/components/_packmol.py +++ b/openff/interchange/components/_packmol.py @@ -134,17 +134,17 @@ def _validate_inputs( """ if ( box_vectors is None - and mass_density is None # noqa: W503 - and (solute is None or solute.box_vectors is None) # noqa: W503 + and mass_density is None + and (solute is None or solute.box_vectors is None) ): raise PACKMOLValueError( "One of `box_vectors`, `mass_density`, or" - + " `solute.box_vectors` must be specified.", # noqa: W503 + + " `solute.box_vectors` must be specified.", ) if box_vectors is not None and mass_density is not None: raise PACKMOLValueError( "`box_vectors` and `mass_density` cannot be specified together;" - + " choose one or the other.", # noqa: W503 + + " choose one or the other.", ) if box_vectors is not None and box_vectors.shape != (3, 3): diff --git a/openff/interchange/components/interchange.py b/openff/interchange/components/interchange.py index 75d070f78..ef13bd5cc 100644 --- a/openff/interchange/components/interchange.py +++ b/openff/interchange/components/interchange.py @@ -56,6 +56,21 @@ class Interchange(_BaseModel): .. warning :: This object is in an early and experimental state and unsuitable for production. .. warning :: This API is experimental and subject to change. + + Examples + -------- + Create an ``Interchange`` from an OpenFF ``ForceField`` and ``Molecule`` + + >>> from openff.toolkit import ForceField, Molecule + >>> sage = ForceField("openff-2.2.0.offxml") + >>> top = Molecule.from_smiles("CCC").to_topology() + >>> interchange = sage.create_interchange(top) + + Get the parameters for the bond between atoms 0 and 1 + + >>> interchange["Bonds"][0, 1] + Potential(...) + """ collections: _AnnotatedCollections = Field(dict()) diff --git a/openff/interchange/components/potentials.py b/openff/interchange/components/potentials.py index c3c368c64..d103abcb7 100644 --- a/openff/interchange/components/potentials.py +++ b/openff/interchange/components/potentials.py @@ -419,6 +419,16 @@ def __getattr__(self, attr: str): else: return super().__getattribute__(attr) + def __getitem__(self, key) -> Potential: + if ( + isinstance(key, tuple) + and key not in self.key_map + and tuple(reversed(key)) in self.key_map + ): + return self.potentials[self.key_map[tuple(reversed(key))]] + + return self.potentials[self.key_map[key]] + def validate_collections( v: Any, diff --git a/openff/interchange/interop/amber/export/_export.py b/openff/interchange/interop/amber/export/_export.py index 818fa4b81..10a2e5d2f 100644 --- a/openff/interchange/interop/amber/export/_export.py +++ b/openff/interchange/interop/amber/export/_export.py @@ -640,7 +640,7 @@ def to_prmtop(interchange: "Interchange", file_path: Path | str): prmtop.write("%FLAG ANGLE_FORCE_CONSTANT\n" "%FORMAT(5E16.8)\n") angle_k = [ interchange["Angles"].potentials[key].parameters["k"].m_as(kcal_mol_rad2) - / 2 # noqa + / 2 for key in potential_key_to_angle_type_mapping ] text_blob = "".join([f"{val:16.8E}" for val in angle_k]) diff --git a/openff/interchange/interop/openmm/__init__.py b/openff/interchange/interop/openmm/__init__.py index fee99b29e..7e911b7d0 100644 --- a/openff/interchange/interop/openmm/__init__.py +++ b/openff/interchange/interop/openmm/__init__.py @@ -204,8 +204,8 @@ def _is_water(molecule: Molecule) -> bool: # TODO: This should only skip rigid waters, even though HMR or flexible water is questionable if ( (hydrogen_atom.atomic_number == 1) - and (heavy_atom.atomic_number != 1) # noqa: W503 - and not (_is_water(hydrogen_atom.molecule)) # noqa: W503 + and (heavy_atom.atomic_number != 1) + and not (_is_water(hydrogen_atom.molecule)) ): hydrogen_index = interchange.topology.atom_index(hydrogen_atom) diff --git a/openff/interchange/models.py b/openff/interchange/models.py index 640874d56..ee07a8c7c 100644 --- a/openff/interchange/models.py +++ b/openff/interchange/models.py @@ -19,9 +19,12 @@ class TopologyKey(_BaseModel, abc.ABC): bond, but not the force constant or equilibrium bond length as determined by the force field. + Topology keys compare equal to (and hash the same as) tuples of their atom + indices as long as their other fields are `None`. + Examples -------- - Create a TopologyKey identifying some speicfic angle + Create a ``TopologyKey`` identifying some specific angle .. code-block:: pycon @@ -30,7 +33,7 @@ class TopologyKey(_BaseModel, abc.ABC): >>> this_angle TopologyKey with atom indices (2, 1, 3) - Create a TopologyKey indentifying just one atom + Create a ``TopologyKey`` indentifying just one atom .. code-block:: pycon @@ -38,6 +41,22 @@ class TopologyKey(_BaseModel, abc.ABC): >>> this_atom TopologyKey with atom indices (4,) + Compare a ``TopologyKey`` to a tuple containing the atom indices + + .. code-block:: pycon + + >>> key = TopologyKey(atom_indices=(0, 1)) + >>> key == (0, 1) + True + + Index into a dictionary with a tuple + + .. code-block:: pycon + + >>> d = {TopologyKey(atom_indices=(0, 1)): "some_bond"} + >>> d[0, 1] + 'some_bond' + """ # TODO: Swith to `pydantic.contuple` once 1.10.3 or 2.0.0 is released @@ -45,11 +64,20 @@ class TopologyKey(_BaseModel, abc.ABC): description="The indices of the atoms occupied by this interaction", ) + def _tuple(self) -> tuple[Any, ...]: + """Tuple representation of this key.""" + return tuple(self.atom_indices) + def __hash__(self) -> int: - return hash(tuple(self.atom_indices)) + return hash(self._tuple()) def __eq__(self, other: Any) -> bool: - return self.__hash__() == other.__hash__() + if isinstance(other, tuple): + return self._tuple() == other + elif isinstance(other, TopologyKey): + return self._tuple() == other._tuple() + else: + return NotImplemented def __repr__(self) -> str: return f"{self.__class__.__name__} with atom indices {self.atom_indices}" @@ -58,9 +86,25 @@ def __repr__(self) -> str: class BondKey(TopologyKey): """ A unique identifier of the atoms associated in a bond potential. + + Examples + -------- + Index into a dictionary with a tuple + + .. code-block:: pycon + + >>> d = { + ... BondKey(atom_indices=(0, 1)): "some_bond", + ... BondKey(atom_indices=(1, 2), bond_order=1.5): "some_other_bond", + ... } + >>> d[0, 1] + 'some_bond' + >>> d[(1, 2), 1.5] + 'some_other_bond' + """ - atom_indices: tuple[int, ...] = Field( + atom_indices: tuple[int, int] = Field( description="The indices of the atoms occupied by this interaction", ) @@ -73,8 +117,11 @@ class BondKey(TopologyKey): ), ) - def __hash__(self) -> int: - return hash((tuple(self.atom_indices), self.bond_order)) + def _tuple(self) -> tuple[int, ...] | tuple[tuple[int, ...], float]: + if self.bond_order is None: + return tuple(self.atom_indices) + else: + return (tuple(self.atom_indices), float(self.bond_order)) def __repr__(self) -> str: return ( @@ -86,19 +133,52 @@ def __repr__(self) -> str: class AngleKey(TopologyKey): """ A unique identifier of the atoms associated in an angle potential. + + Examples + -------- + Index into a dictionary with a tuple + + .. code-block:: pycon + + >>> d = {AngleKey(atom_indices=(0, 1, 2)): "some_angle"} + >>> d[0, 1, 2] + 'some_angle' + """ - atom_indices: tuple[int, ...] = Field( + atom_indices: tuple[int, int, int] = Field( description="The indices of the atoms occupied by this interaction", ) + def _tuple(self) -> tuple[int, ...]: + return tuple(self.atom_indices) + class ProperTorsionKey(TopologyKey): """ A unique identifier of the atoms associated in a proper torsion potential. + + Examples + -------- + Index into a dictionary with a tuple + + .. code-block:: pycon + + >>> d = { + ... ProperTorsionKey(atom_indices=(0, 1, 2, 3)): "torsion1", + ... ProperTorsionKey(atom_indices=(0, 1, 2, 3), mult=2): "torsion2", + ... ProperTorsionKey(atom_indices=(5, 6, 7, 8), mult=2, phase=0.78, bond_order=1.5): "torsion3", + ... } + >>> d[0, 1, 2, 3] + 'torsion1' + >>> d[(0, 1, 2, 3), 2, None, None] + 'torsion2' + >>> d[(5, 6, 7, 8), 2, 0.78, 1.5] + 'torsion3' + """ - atom_indices: tuple[int, ...] = Field( + atom_indices: tuple[int, int, int, int] | tuple[()] = Field( description="The indices of the atoms occupied by this interaction", ) @@ -123,8 +203,22 @@ class ProperTorsionKey(TopologyKey): ), ) - def __hash__(self) -> int: - return hash((tuple(self.atom_indices), self.mult, self.bond_order, self.phase)) + def _tuple( + self, + ) -> ( + tuple[()] + | tuple[int, int, int, int] + | tuple[ + tuple[int, int, int, int] | tuple[()], + int | None, + float | None, + float | None, + ] + ): + if self.mult is None and self.phase is None and self.bond_order is None: + return tuple(self.atom_indices) + else: + return (tuple(self.atom_indices), self.mult, self.phase, self.bond_order) def __repr__(self) -> str: return ( @@ -139,6 +233,25 @@ class ImproperTorsionKey(ProperTorsionKey): A unique identifier of the atoms associated in an improper torsion potential. The central atom is the second atom in the `atom_indices` tuple, or accessible via `get_central_atom_index`. + + Examples + -------- + Index into a dictionary with a tuple + + .. code-block:: pycon + + >>> d = { + ... ImproperTorsionKey(atom_indices=(0, 1, 2, 3)): "torsion1", + ... ImproperTorsionKey(atom_indices=(0, 1, 2, 3), mult=2): "torsion2", + ... ImproperTorsionKey(atom_indices=(5, 6, 7, 8), mult=2, phase=0.78, bond_order=1.5): "torsion3", + ... } + >>> d[0, 1, 2, 3] + 'torsion1' + >>> d[(0, 1, 2, 3), 2, None, None] + 'torsion2' + >>> d[(5, 6, 7, 8), 2, 0.78, 1.5] + 'torsion3' + """ def get_central_atom_index(self) -> int: @@ -157,15 +270,15 @@ class LibraryChargeTopologyKey(_BaseModel): this_atom_index: int @property - def atom_indices(self) -> tuple[int, ...]: + def atom_indices(self) -> tuple[int]: """Alias for `this_atom_index`.""" return (self.this_atom_index,) def __hash__(self) -> int: return hash((self.this_atom_index,)) - def __eq__(self, other: Any) -> bool: - return self.__hash__() == other.__hash__() + def __eq__(self, other) -> bool: + return super().__eq__(other) or other == self.this_atom_index class SingleAtomChargeTopologyKey(LibraryChargeTopologyKey): @@ -206,7 +319,9 @@ def __hash__(self) -> int: class VirtualSiteKey(TopologyKey): - """A unique identifier of a virtual site in the scope of a chemical topology.""" + """ + A unique identifier of a virtual site in the scope of a chemical topology. + """ # TODO: Overriding the attribute of a parent class is clumsy, but less grief than # having this not inherit from `TopologyKey`. It might be useful to just have @@ -224,14 +339,12 @@ class VirtualSiteKey(TopologyKey): description="The `match` attribute of the associated virtual site type", ) - def __hash__(self) -> int: - return hash( - ( - self.orientation_atom_indices, - self.name, - self.type, - self.match, - ), + def _tuple(self) -> tuple[tuple[int, ...], str, str, str]: + return ( + self.orientation_atom_indices, + self.name, + self.type, + self.match, ) diff --git a/openff/interchange/smirnoff/_gromacs.py b/openff/interchange/smirnoff/_gromacs.py index fe683aabe..a09622c7f 100644 --- a/openff/interchange/smirnoff/_gromacs.py +++ b/openff/interchange/smirnoff/_gromacs.py @@ -761,8 +761,8 @@ def _is_water(molecule: Molecule) -> bool: # TODO: This should only skip rigid waters, even though HMR or flexible water is questionable if ( (hydrogen_atom.atomic_number == 1) - and (heavy_atom.atomic_number != 1) # noqa: W503 - and not (_is_water(hydrogen_atom.molecule)) # noqa: W503 + and (heavy_atom.atomic_number != 1) + and not (_is_water(hydrogen_atom.molecule)) ): # these are molecule indices, whereas in the OpenMM function they are topology indices diff --git a/openff/interchange/smirnoff/_virtual_sites.py b/openff/interchange/smirnoff/_virtual_sites.py index d9207cc2c..c4b297611 100644 --- a/openff/interchange/smirnoff/_virtual_sites.py +++ b/openff/interchange/smirnoff/_virtual_sites.py @@ -446,8 +446,8 @@ def _convert_local_coordinates( # rather than 0 degrees from the z-axis. vsite_positions = local_coordinate_frames[0] + d * ( cos_theta * cos_phi * local_coordinate_frames[1] - + sin_theta * cos_phi * local_coordinate_frames[2] # noqa - + sin_phi * local_coordinate_frames[3] # noqa + + sin_theta * cos_phi * local_coordinate_frames[2] + + sin_phi * local_coordinate_frames[3] ) return vsite_positions diff --git a/setup.cfg b/setup.cfg index 7652dc8c4..3bc8ba763 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,7 +12,7 @@ exclude_lines = [flake8] max-line-length = 119 -ignore = E203,B028 +ignore = E203,B028,W503 per-file-ignores = openff/interchange/_tests/unit_tests/test_types.py:F821 openff/interchange/**/__init__.py:F401