Openff-toolkit: to/from_openeye can not distinguish None vs. all-zero partial charges

Created on 20 Feb 2020  路  2Comments  路  Source: openforcefield/openff-toolkit

Background

The specifications for each package's handling of partial charges is as follows:

  • OpenFF Toolkit: mol.partial_charges can be None or a Quantity-wrapped numpy array of floats
  • OpenEye: Each OEAtom has a Get/SetPartialCharge method, which accepts floats. Partial charges are initialized to 0.0 and can not be None.
  • RDKit: Each RDAtom can additional SD data using rdk_atom.SetDoubleProp('PartialCharge', value)
  • SDF spec we're adopting (#250): A <atom.dprop.PartialCharges> tag can be included, with a space-separated list of doubles (and maybe special n/a values to indicate that some atoms dont have charges).

The OpenFF partial charge spec has the following equivalents in these representations:

  • if offmol.partial_charges is None

    • OpenEye: ~No equivalent~ (Per @SimonBoothroyd's comment below, this can handle float('nan')

    • RDKit: RDAtoms don't have PartialCharges set

    • SDF: No <atom.dprop.PartialCharges> tag is included

  • if offmol.partial_charges is not None:

    • OpenEye: OEAtoms have the appropriate PartialCharges set

    • RDKit: RDAtoms have the appropriate PartialCharges set

    • SDF: The <atom.dprop.PartialCharges> tag is included, with no values of N/A

Problem

OFFMol roundtrips to OEMols can't preserve whether partial charges were None or all-zero.

Current status

The above roundtrip will return all-zero partial_charges from OpenEye, even if the original OFFMol had partial_charges is None.

In #281 and previous work, I've implemented tests for the above roundtrips. All of them can successfully roundtrip None, except OpenEye, which returns all zeroes.

(For SDF roundtrips using OpenEye, I implemented special logic to query for the presence or absence of the <atom.dprop.PartialCharges> tag in the SDF, and so None partial charges can actually survive that roundtrip.)

Options for moving forward

We can not proceed forever with equating "all zero partial charges" and "partial_charges is None". None and "all-zeroes" have different meanings for OFFTK, and seemingly-reasonable assumptions about their equivalence/meaning have caused us to produce surprising behavior in situations like #381 and barriers in implementation in #488. Further, leaving the distinction ambiguous will cause serious headaches the day someone tries to put truly zero-partial-charge mols like H2, N2, O2, noble gas, or buckyballs through OFFTK.

The best option I can think of quickly is a "nasty hack", such as setting a special SD tag on the OEMol that comes out of offmol.to_openeye that distinguishes whether partial_charges is None or really all-zero.

I would really like to hear if anyone can think of a less hacky solution!

Timeframe

This behavior has always been broken in OFFTK, so it's not critical that we come to a conclusion urgently.

Most helpful comment

Not ideal, but can you set the OpenEye charges to be NaN as a substitute for None?

All 2 comments

Not ideal, but can you set the OpenEye charges to be NaN as a substitute for None?

Brilliant, @SimonBoothroyd!

from openeye import oechem
oemol = oechem.OEMol()
oechem.OESmilesToMol(oemol, "C")
for atom in oemol.GetAtoms():
    print(atom.GetPartialCharge())
    atom.SetPartialCharge(float('nan'))
    print(atom.GetPartialCharge())

0.0
nan

Was this page helpful?
0 / 5 - 0 ratings