Skip to content

Commit

Permalink
allow float multiplicity
Browse files Browse the repository at this point in the history
  • Loading branch information
loriab committed Sep 18, 2024
1 parent 936407e commit efb92a0
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 17 deletions.
14 changes: 8 additions & 6 deletions qcelemental/models/molecule.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ class Molecule(ProtoModel):
description="Additional comments for this molecule. Intended for pure human/user consumption and clarity.",
)
molecular_charge: float = Field(0.0, description="The net electrostatic charge of the molecule.") # type: ignore
molecular_multiplicity: int = Field(1, description="The total multiplicity of the molecule.") # type: ignore
molecular_multiplicity: float = Field(1, description="The total multiplicity of the molecule.") # type: ignore

# Atom data
masses_: Optional[Array[float]] = Field( # type: ignore
Expand Down Expand Up @@ -257,7 +257,7 @@ class Molecule(ProtoModel):
"if not provided (and :attr:`~qcelemental.models.Molecule.fragments` are specified).",
shape=["nfr"],
)
fragment_multiplicities_: Optional[List[int]] = Field( # type: ignore
fragment_multiplicities_: Optional[List[float]] = Field( # type: ignore
None,
description="The multiplicity of each fragment in the :attr:`~qcelemental.models.Molecule.fragments` list. The index of this "
"list matches the 0-index indices of :attr:`~qcelemental.models.Molecule.fragments` list. Will be filled in based on a set of "
Expand Down Expand Up @@ -421,12 +421,16 @@ def _must_be_n_frag_mult(cls, v, values, **kwargs):
n = len(values["fragments_"])
if len(v) != n:
raise ValueError("Fragment Multiplicities must be same number of entries as Fragments")
v = [(int(m) if m.is_integer() else m) for m in v]
if any([m < 1.0 for m in v]):
raise ValueError(f"Fragment Multiplicity must be positive: {v}")
return v

@validator("molecular_multiplicity")
def _int_if_possible(cls, v, values, **kwargs):
if v.is_integer():
# preserve existing hashes
v = int(v)
if v < 1.0:
raise ValueError("Molecular Multiplicity must be positive")
return v
Expand Down Expand Up @@ -502,7 +506,7 @@ def fragment_charges(self) -> List[float]:
return fragment_charges

@property
def fragment_multiplicities(self) -> List[int]:
def fragment_multiplicities(self) -> List[float]:
fragment_multiplicities = self.__dict__.get("fragment_multiplicities_")
if fragment_multiplicities is None:
fragment_multiplicities = [self.molecular_multiplicity]
Expand Down Expand Up @@ -803,9 +807,7 @@ def get_hash(self):
data = getattr(self, field)
if field == "geometry":
data = float_prep(data, GEOMETRY_NOISE)
elif field == "fragment_charges":
data = float_prep(data, CHARGE_NOISE)
elif field == "molecular_charge":
elif field in ["fragment_charges", "molecular_charge", "fragment_multiplicities", "molecular_multiplicity"]:
data = float_prep(data, CHARGE_NOISE)
elif field == "masses":
data = float_prep(data, MASS_NOISE)
Expand Down
20 changes: 17 additions & 3 deletions qcelemental/molparse/chgmult.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def _high_spin_sum(mult_list):


def _mult_ok(m):
return isinstance(m, (int, np.integer)) and m >= 1
return isinstance(m, (int, np.integer, float, np.float64)) and m >= 1


def _sufficient_electrons_for_mult(z, c, m):
Expand Down Expand Up @@ -430,7 +430,14 @@ def int_if_possible(val):
if molecular_multiplicity is None: # unneeded, but shortens the exact lists
frag_mult_hi = _high_spin_sum(_apply_default(fragment_multiplicities, 2))
frag_mult_lo = _high_spin_sum(_apply_default(fragment_multiplicities, 1))
for m in range(frag_mult_lo, frag_mult_hi + 1):
try:
mult_range = range(frag_mult_lo, frag_mult_hi + 1)
except TypeError:
if frag_mult_lo == frag_mult_hi:
mult_range = [frag_mult_hi]
else:
raise ValidationError(f"Cannot process: please fully specify float multiplicity: m: {molecular_multiplicity} fm: {fragment_multiplicities}")
for m in mult_range:
cgmp_exact_m.append(m)

# * (S6) suggest range of missing mult = tot - high_spin_sum(frag - 1),
Expand All @@ -450,7 +457,14 @@ def int_if_possible(val):

for ifr in range(nfr):
if fragment_multiplicities[ifr] is None: # unneeded, but shortens the exact lists
for m in reversed(range(max(missing_mult_lo, 1), missing_mult_hi + 1)):
try:
mult_range = reversed(range(max(missing_mult_lo, 1), missing_mult_hi + 1))
except TypeError:
if missing_mult_lo == missing_mult_hi:
mult_range = [missing_mult_hi]
else:
raise ValidationError(f"Cannot process: please fully specify float multiplicity: m: {molecular_multiplicity} fm: {fragment_multiplicities}")
for m in mult_range:
cgmp_exact_fm[ifr].append(m)
cgmp_exact_fm[ifr].append(1)
cgmp_exact_fm[ifr].append(2)
Expand Down
46 changes: 38 additions & 8 deletions qcelemental/tests/test_molecule.py
Original file line number Diff line number Diff line change
Expand Up @@ -798,6 +798,15 @@ def test_extras():
"triplet": "7caca87a",
"disinglet": "83a85546",
"ditriplet": "71d6ba82",
# float mult
"singlet_point1": "4e9e2587",
"singlet_epsilon": "ad3f5fab",
"triplet_point1": "ad35cc28",
"triplet_point1_minus": "b63d6983",
"triplet_point00001": "7107b7ac",
"disinglet_epsilon": "fb0aaaca",
"ditriplet_point1": "33d47d5f",
"ditriplet_point00001": "7f0ac640",
}


Expand All @@ -806,14 +815,26 @@ def test_extras():
[
pytest.param(3, 3, False, "triplet"),
pytest.param(3, 3, True, "triplet"),
# 3.1 -> 3 (validate=False) below documents the present bad behavior where a float mult
# simply gets cast to int with no error. This will change soon. The validate=True throws a
# nonspecific error that at least mentions type.
pytest.param(3.1, 3, False, "triplet"),
# before float multiplicity was allowed, 3.1 (below) was coerced into 3 with validate=False,
# and validate=True threw a type-mentioning error. Now, 2.9 is allowed for both validate=T/F
pytest.param(3.1, 3.1, False, "triplet_point1"),
# validate=True counterpart fails b/c insufficient electrons in He for more than triplet
pytest.param(2.9, 2.9, False, "triplet_point1_minus"),
pytest.param(2.9, 2.9, True, "triplet_point1_minus"),
pytest.param(3.00001, 3.00001, False, "triplet_point00001"),
# validate=True counterpart fails like 3.1 above
pytest.param(2.99999, 2.99999, False, "triplet_point00001"), # hash agrees w/3.00001 above b/c <CHARGE_NOISE
pytest.param(2.99999, 2.99999, True, "triplet_point00001"),
pytest.param(3.0, 3, False, "triplet"),
pytest.param(3.0, 3, True, "triplet"),
pytest.param(1, 1, False, "singlet"),
pytest.param(1, 1, True, "singlet"),
pytest.param(1.000000000000000000002, 1, False, "singlet"),
pytest.param(1.000000000000000000002, 1, True, "singlet"),
pytest.param(1.000000000000002, 1.000000000000002, False, "singlet_epsilon"),
pytest.param(1.000000000000002, 1.000000000000002, True, "singlet_epsilon"),
pytest.param(1.1, 1.1, False, "singlet_point1"),
pytest.param(1.1, 1.1, True, "singlet_point1"),
pytest.param(None, 1, False, "singlet"),
pytest.param(None, 1, True, "singlet"),
# fmt: off
Expand Down Expand Up @@ -841,6 +862,9 @@ def test_mol_multiplicity_types(mult_in, mult_store, validate, exp_hash):
[
pytest.param(-3, False, "Multiplicity must be positive"),
pytest.param(-3, True, "Multiplicity must be positive"),
pytest.param(0.9, False, "Multiplicity must be positive"),
pytest.param(0.9, True, "Multiplicity must be positive"),
pytest.param(3.1, True, "Inconsistent or unspecified chg/mult"), # insufficient electrons in He
],
)
def test_mol_multiplicity_types_errors(mult_in, validate, error):
Expand All @@ -859,10 +883,11 @@ def test_mol_multiplicity_types_errors(mult_in, validate, error):
[
pytest.param(5, [3, 3], [3, 3], False, "ditriplet"),
pytest.param(5, [3, 3], [3, 3], True, "ditriplet"),
# 3.1 -> 3 (validate=False) below documents the present bad behavior where a float mult
# simply gets cast to int with no error. This will change soon. The validate=True throws a
# irreconcilable error.
pytest.param(5, [3.1, 3.4], [3, 3], False, "ditriplet"),
# before float multiplicity was allowed, [3.1, 3.4] (below) were coerced into [3, 3] with validate=False.
# Now, [2.9, 2.9] is allowed for both validate=T/F.
pytest.param(5, [3.1, 3.4], [3.1, 3.4], False, "ditriplet_point1"),
pytest.param(5, [2.99999, 3.00001], [2.99999, 3.00001], False, "ditriplet_point00001"),
pytest.param(5, [2.99999, 3.00001], [2.99999, 3.00001], True, "ditriplet_point00001"),
# fmt: off
pytest.param(5, [3.0, 3.], [3, 3], False, "ditriplet"),
pytest.param(5, [3.0, 3.], [3, 3], True, "ditriplet"),
Expand All @@ -871,6 +896,10 @@ def test_mol_multiplicity_types_errors(mult_in, validate, error):
pytest.param(1, [1, 1], [1, 1], True, "disinglet"),
# None in frag_mult not allowed for validate=False
pytest.param(1, [None, None], [1, 1], True, "disinglet"),
pytest.param(1, [1.000000000000000000002, 0.999999999999999999998], [1, 1], False, "disinglet"),
pytest.param(1, [1.000000000000000000002, 0.999999999999999999998], [1, 1], True, "disinglet"),
pytest.param(1, [1.000000000000002, 1.000000000000004], [1.000000000000002, 1.000000000000004], False, "disinglet_epsilon"),
pytest.param(1, [1.000000000000002, 1.000000000000004], [1.000000000000002, 1.000000000000004], True, "disinglet_epsilon"),
],
)
def test_frag_multiplicity_types(mol_mult_in, mult_in, mult_store, validate, exp_hash):
Expand Down Expand Up @@ -902,6 +931,7 @@ def test_frag_multiplicity_types(mol_mult_in, mult_in, mult_store, validate, exp
[
pytest.param([-3, 1], False, "Multiplicity must be positive"),
pytest.param([-3, 1], True, "Multiplicity must be positive"),
pytest.param([3.1, 3.4], True, "Inconsistent or unspecified chg/mult"), # insufficient e- for triplet+ on He in frag 1
],
)
def test_frag_multiplicity_types_errors(mult_in, validate, error):
Expand Down
10 changes: 10 additions & 0 deletions qcelemental/tests/test_molparse_validate_and_fill_chgmult.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@
(-2.4, [-2.4, 0, 0], 3, [1, 2, 2]),
"a83a3356",
), # 166
(("He", None, [None], 2.8, [None]), (0, [0], 2.8, [2.8]), "3e10e7b5"), # 180
(("He", None, [None], None, [2.8]), (0, [0], 2.8, [2.8]), "3e10e7b5"), # 181
(("N/N/N", None, [None, None, None], 2.2, [2, 2, 2.2]), (0, [0, 0, 0], 2.2, [2, 2, 2.2]), "798ee5d4"), # 183
(("N/N/N", None, [None, None, None], 4.2, [2, 2, 2.2]), (0, [0, 0, 0], 4.2, [2, 2, 2.2]), "ed6d1f35"), # 185
(("N/N/N", None, [None, None, None], None, [2, 2, 2.2]), (0, [0, 0, 0], 4.2, [2, 2, 2.2]), "ed6d1f35"), # 186
(("N/N/N", None, [2, -2, None], 2.2, [2, 2, 2.2]), (0, [2, -2, 0], 2.2, [2, 2, 2.2]), "66e655c0"), # 187
]


Expand Down Expand Up @@ -153,6 +159,8 @@ def none_y(inp):
("Gh", None, [None], 3, [None]), # 60
("Gh/He", None, [2, None], None, [None, None]), # 62
("Gh/Ne", 2, [-2, None], None, [None, None]), # 65b
("He", None, [None], 3.2, [None]), # 182
("N/N/N", None, [None, None, None], 2.2, [None, None, 2.2]), # 184
],
)
def test_validate_and_fill_chgmult_irreconcilable(systemtranslator, inp):
Expand All @@ -173,6 +181,8 @@ def test_validate_and_fill_chgmult_irreconcilable(systemtranslator, inp):
# 35 - insufficient electrons
# 55 - both (1, (1, 0.0, 0.0), 4, (1, 3, 2)) and (1, (0.0, 0.0, 1), 4, (2, 3, 1)) plausible
# 65 - non-0/1 on Gh fragment errors normally but reset by zero_ghost_fragments
# 182 - insufficient electrons on He
# 184 - decline to guess fragment multiplicities when floats involved


@pytest.fixture
Expand Down

0 comments on commit efb92a0

Please sign in to comment.