From 506c7914076dea8e201555fc8a578a1c58da6b33 Mon Sep 17 00:00:00 2001 From: Sebastian Ehlert Date: Thu, 3 Aug 2023 12:31:38 +0000 Subject: [PATCH 1/2] Allow floating point numbers for multiplicities --- qcelemental/models/molecule.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/qcelemental/models/molecule.py b/qcelemental/models/molecule.py index 83eb4a35..a3398e2e 100644 --- a/qcelemental/models/molecule.py +++ b/qcelemental/models/molecule.py @@ -179,7 +179,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: Union[int, float] = Field(1, description="The total multiplicity of the molecule.") # type: ignore # Atom data masses_: Optional[Array[float]] = Field( # type: ignore @@ -251,7 +251,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[Union[int, 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 " @@ -784,6 +784,12 @@ def get_hash(self): data = float_prep(data, CHARGE_NOISE) elif field == "molecular_charge": data = float_prep(data, CHARGE_NOISE) + elif field == "fragment_multiplicities": + if any(isinstance(value, float) for value in data): + data = float_prep(data, CHARGE_NOISE) + elif field == "molecular_multiplicity": + if isinstance(data, float): + data = float_prep(data, CHARGE_NOISE) elif field == "masses": data = float_prep(data, MASS_NOISE) From 01068567dcfc05b394eff0c0405f142c47e44037 Mon Sep 17 00:00:00 2001 From: "Lori A. Burns" Date: Sat, 30 Sep 2023 01:45:07 -0400 Subject: [PATCH 2/2] test float mult --- qcelemental/models/molecule.py | 25 +++++++++++++++++++---- qcelemental/molparse/chgmult.py | 15 ++++++++++++++ qcelemental/tests/test_molecule.py | 32 ++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 4 deletions(-) diff --git a/qcelemental/models/molecule.py b/qcelemental/models/molecule.py index a3398e2e..1c57b404 100644 --- a/qcelemental/models/molecule.py +++ b/qcelemental/models/molecule.py @@ -179,7 +179,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: Union[int, float] = 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 @@ -251,7 +251,7 @@ class Molecule(ProtoModel): "if not provided (and :attr:`~qcelemental.models.Molecule.fragments` are specified).", shape=["nfr"], ) - fragment_multiplicities_: Optional[List[Union[int, float]]] = 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 " @@ -397,7 +397,7 @@ def _populate_real(cls, v, values, **kwargs): v = np.array([True for _ in range(n)]) return v - @validator("fragment_charges_", "fragment_multiplicities_") + @validator("fragment_charges_") def _must_be_n_frag(cls, v, values, **kwargs): if "fragments_" in values and values["fragments_"] is not None: n = len(values["fragments_"]) @@ -407,6 +407,23 @@ def _must_be_n_frag(cls, v, values, **kwargs): ) return v + @validator("fragment_multiplicities_") + def _must_be_n_frag_mult(cls, v, values, **kwargs): + if "fragments_" in values and values["fragments_"] is not None: + n = len(values["fragments_"]) + if len(v) != n: + raise ValueError( + "Fragment Charges and Fragment Multiplicities must be same number of entries as Fragments" + ) + int_ized_v = [(int(m) if m.is_integer() else m) for m in v] + return int_ized_v + + @validator("molecular_multiplicity") + def _int_if_possible(cls, v, values, **kwargs): + if v.is_integer(): + v = int(v) + return v + @property def hash_fields(self): return [ @@ -478,7 +495,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] diff --git a/qcelemental/molparse/chgmult.py b/qcelemental/molparse/chgmult.py index 24cf1ffa..3634c248 100644 --- a/qcelemental/molparse/chgmult.py +++ b/qcelemental/molparse/chgmult.py @@ -101,6 +101,8 @@ def validate_and_fill_chgmult( ------ qcelemental.ValidationError When no solution to input arguments subject to the constraints below can be found. + TypeError + When fractional multiplicity is provided. Notes ----- @@ -300,6 +302,19 @@ def validate_and_fill_chgmult( log_brief = verbose >= 2 # TODO: Move back to 1 text = [] + def int_if_possible(val): + if isinstance(val, float) and val.is_integer(): + return int(val) + else: + return val + + molecular_multiplicity = int_if_possible(molecular_multiplicity) + fragment_multiplicities = [int_if_possible(m) for m in fragment_multiplicities] + if isinstance(molecular_multiplicity, float) or any(isinstance(m, float) for m in fragment_multiplicities): + raise TypeError( + f"validate_and_fill_chgmult() cannot handle fractional multiplicity. m: {molecular_multiplicity}, fm: {fragment_multiplicities}" + ) + felez = np.split(zeff, fragment_separators) nfr = len(felez) if log_full: diff --git a/qcelemental/tests/test_molecule.py b/qcelemental/tests/test_molecule.py index 1d7b82ab..c22c4a94 100644 --- a/qcelemental/tests/test_molecule.py +++ b/qcelemental/tests/test_molecule.py @@ -734,3 +734,35 @@ def test_extras(): mol = qcel.models.Molecule(symbols=["He"], geometry=[0, 0, 0], extras={"foo": "bar"}) assert mol.extras["foo"] == "bar" + + +@pytest.mark.parametrize( + "mult_in,mult_store,validate", + [ + pytest.param(3, 3, False), + pytest.param(3, 3, True), + pytest.param(3.1, 3.1, False), + pytest.param(3.00001, 3.00001, False), + pytest.param(3.0, 3, False), + pytest.param(3.0, 3, True), + pytest.param(1, 1, False), + pytest.param(1, 1, True), + pytest.param(1.000000000000000000002, 1, False), + pytest.param(1.000000000000000000002, 1, True), + pytest.param(1.000000000000002, 1.000000000000002, False), + pytest.param(None, 1, False), + pytest.param(None, 1, True), + ], +) +def test_mol_multiplicity_types(mult_in, mult_store, validate): + # validate=False passes through pydantic validators. =True passes through molparse. + # fractional can only use =False route b/c molparse can't check the physics of chg/mult for float multiplicity. + if mult_in is None: + mol = qcel.models.Molecule(symbols=["He"], geometry=[0, 0, 0], validate=validate) + else: + mol = qcel.models.Molecule( + symbols=["He"], geometry=[0, 0, 0], validate=validate, molecular_multiplicity=mult_in + ) + + assert mult_store == mol.molecular_multiplicity + assert type(mult_store) is type(mol.molecular_multiplicity)