From 5189f2d8d1263ae180242123e64fcc534bfa25e6 Mon Sep 17 00:00:00 2001 From: "Lori A. Burns" Date: Tue, 24 Sep 2024 10:34:47 -0400 Subject: [PATCH 1/5] input/result converters --- docs/changelog.rst | 4 ++ qcelemental/datum.py | 2 +- qcelemental/models/v1/common_models.py | 9 +++ qcelemental/models/v1/procedures.py | 96 ++++++++++++++++++++++++-- qcelemental/models/v1/results.py | 62 ++++++++++++++++- qcelemental/models/v2/common_models.py | 9 +++ qcelemental/models/v2/procedures.py | 96 ++++++++++++++++++++++++-- qcelemental/models/v2/results.py | 62 ++++++++++++++++- 8 files changed, 322 insertions(+), 18 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index a2c221a7..201903b9 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -35,6 +35,10 @@ New Features Enhancements ++++++++++++ +* ``AtomicInput`` and ``AtomicResult`` ``OptimizationInput``, ``OptimizationResult``, ``TorsionDriveInput``, ``TorsionDriveResult`` (both versions) learned a ``.convert_v(ver)`` function that returns self or the other version. +* The ``models.v2`` ``AtomicInput``, ``AtomicResult``, ``AtomicResultProperties`` ``OptimizationInput``, ``OptimizationResult``, ``TorsionDriveInput``, ``TorsionDriveResult`` had their `schema_version` changed to a Literal[2] and validated so new instances will be 2, even if another value passed in. +* The ``models.v1`` ``AtomicInput``, ``AtomicResult``, ``OptimizationInput``, ``OptimizationResult``, ``TorsionDriveInput``, ``TorsionDriveResult`` had their `schema_version` changed to a Literal[1] and validated so new instances will be 1, even if another value passed in. +* The ``models.v1`` and ``models.v2`` ``OptimizationResult`` given schema_version for the first time. * The ``models.v2`` have had their `schema_version` bumped for ``BasisSet``, ``AtomicInput``, ``OptimizationInput`` (implicit for ``AtomicResult`` and ``OptimizationResult``), ``TorsionDriveInput`` , and ``TorsionDriveResult``. * The ``models.v2`` ``AtomicResultProperties`` has been given a ``schema_name`` and ``schema_version`` (2) for the first time. * Note that ``models.v2`` ``QCInputSpecification`` and ``OptimizationSpecification`` have *not* had schema_version bumped. diff --git a/qcelemental/datum.py b/qcelemental/datum.py index 5ca348e3..fbc8285d 100644 --- a/qcelemental/datum.py +++ b/qcelemental/datum.py @@ -163,7 +163,7 @@ def dict(self, *args, **kwargs): def json(self, *args, **kwargs): """ - Passthrough to model_dump_sjon without deprecation warning + Passthrough to model_dump_json without deprecation warning exclude_unset is forced through the model_serializer """ return super().model_dump_json(*args, **kwargs) diff --git a/qcelemental/models/v1/common_models.py b/qcelemental/models/v1/common_models.py index 7f822798..e27b916a 100644 --- a/qcelemental/models/v1/common_models.py +++ b/qcelemental/models/v1/common_models.py @@ -129,6 +129,15 @@ def __repr_args__(self) -> "ReprArgs": return [("error", self.error)] +def check_convertible_version(ver: int, error: str): + if ver == 1: + return "self" + elif ver == 2: + return True + else: + raise ValueError(f"QCSchema {error} version={version} does not exist for conversion.") + + qcschema_input_default = "qcschema_input" qcschema_output_default = "qcschema_output" qcschema_optimization_input_default = "qcschema_optimization_input" diff --git a/qcelemental/models/v1/procedures.py b/qcelemental/models/v1/procedures.py index 5a0ce95b..7b9717bc 100644 --- a/qcelemental/models/v1/procedures.py +++ b/qcelemental/models/v1/procedures.py @@ -1,5 +1,11 @@ from enum import Enum -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union + +try: + from typing import Literal +except ImportError: + # remove when minimum py38 + from typing_extensions import Literal from pydantic.v1 import Field, conlist, constr, validator @@ -10,6 +16,7 @@ DriverEnum, Model, Provenance, + check_convertible_version, qcschema_input_default, qcschema_optimization_input_default, qcschema_optimization_output_default, @@ -53,7 +60,7 @@ class QCInputSpecification(ProtoModel): """ schema_name: constr(strip_whitespace=True, regex=qcschema_input_default) = qcschema_input_default # type: ignore - schema_version: int = 1 + schema_version: int = 1 # TODO driver: DriverEnum = Field(DriverEnum.gradient, description=str(DriverEnum.__doc__)) model: Model = Field(..., description=str(Model.__doc__)) @@ -71,7 +78,7 @@ class OptimizationInput(ProtoModel): schema_name: constr( # type: ignore strip_whitespace=True, regex=qcschema_optimization_input_default ) = qcschema_optimization_input_default - schema_version: int = 1 + schema_version: Literal[1] = 1 keywords: Dict[str, Any] = Field({}, description="The optimization specific keywords to be used.") extras: Dict[str, Any] = Field({}, description="Extra fields that are not part of the schema.") @@ -88,11 +95,31 @@ def __repr_args__(self) -> "ReprArgs": ("molecule_hash", self.initial_molecule.get_hash()[:7]), ] + @validator("schema_version", pre=True) + def _version_stamp(cls, v): + return 1 + + def convert_v( + self, version: int + ) -> Union["qcelemental.models.v1.OptimizationInput", "qcelemental.models.v2.OptimizationInput"]: + """Convert to instance of particular QCSchema version.""" + import qcelemental as qcel + + if check_convertible_version(version, error="OptimizationInput") == "self": + return self + + dself = self.dict() + if version == 2: + self_vN = qcel.models.v2.OptimizationInput(**dself) + + return self_vN + class OptimizationResult(OptimizationInput): schema_name: constr( # type: ignore strip_whitespace=True, regex=qcschema_optimization_output_default ) = qcschema_optimization_output_default + schema_version: Literal[1] = 1 final_molecule: Optional[Molecule] = Field(..., description="The final molecule of the geometry optimization.") trajectory: List[AtomicResult] = Field( @@ -131,6 +158,25 @@ def _trajectory_protocol(cls, v, values): return v + @validator("schema_version", pre=True) + def _version_stamp(cls, v): + return 1 + + def convert_v( + self, version: int + ) -> Union["qcelemental.models.v1.OptimizationResult", "qcelemental.models.v2.OptimizationResult"]: + """Convert to instance of particular QCSchema version.""" + import qcelemental as qcel + + if check_convertible_version(version, error="OptimizationResult") == "self": + return self + + dself = self.dict() + if version == 2: + self_vN = qcel.models.v2.OptimizationResult(**dself) + + return self_vN + class OptimizationSpecification(ProtoModel): """ @@ -143,7 +189,7 @@ class OptimizationSpecification(ProtoModel): """ schema_name: constr(strip_whitespace=True, regex="qcschema_optimization_specification") = "qcschema_optimization_specification" # type: ignore - schema_version: int = 1 + schema_version: int = 1 # TODO procedure: str = Field(..., description="Optimization procedure to run the optimization with.") keywords: Dict[str, Any] = Field({}, description="The optimization specific keywords to be used.") @@ -200,7 +246,7 @@ class TorsionDriveInput(ProtoModel): """ schema_name: constr(strip_whitespace=True, regex=qcschema_torsion_drive_input_default) = qcschema_torsion_drive_input_default # type: ignore - schema_version: int = 1 + schema_version: Literal[1] = 1 keywords: TDKeywords = Field(..., description="The torsion drive specific keywords to be used.") extras: Dict[str, Any] = Field({}, description="Extra fields that are not part of the schema.") @@ -221,6 +267,25 @@ def _check_input_specification(cls, value): assert value.driver == DriverEnum.gradient, "driver must be set to gradient" return value + @validator("schema_version", pre=True) + def _version_stamp(cls, v): + return 1 + + def convert_v( + self, version: int + ) -> Union["qcelemental.models.v1.TorsionDriveInput", "qcelemental.models.v2.TorsionDriveInput"]: + """Convert to instance of particular QCSchema version.""" + import qcelemental as qcel + + if check_convertible_version(version, error="TorsionDriveInput") == "self": + return self + + dself = self.dict() + if version == 2: + self_vN = qcel.models.v2.TorsionDriveInput(**dself) + + return self_vN + class TorsionDriveResult(TorsionDriveInput): """Results from running a torsion drive. @@ -231,7 +296,7 @@ class TorsionDriveResult(TorsionDriveInput): """ schema_name: constr(strip_whitespace=True, regex=qcschema_torsion_drive_output_default) = qcschema_torsion_drive_output_default # type: ignore - schema_version: int = 1 + schema_version: Literal[1] = 1 final_energies: Dict[str, float] = Field( ..., description="The final energy at each angle of the TorsionDrive scan." @@ -254,6 +319,25 @@ class TorsionDriveResult(TorsionDriveInput): error: Optional[ComputeError] = Field(None, description=str(ComputeError.__doc__)) provenance: Provenance = Field(..., description=str(Provenance.__doc__)) + @validator("schema_version", pre=True) + def _version_stamp(cls, v): + return 1 + + def convert_v( + self, version: int + ) -> Union["qcelemental.models.v1.TorsionDriveResult", "qcelemental.models.v2.TorsionDriveResult"]: + """Convert to instance of particular QCSchema version.""" + import qcelemental as qcel + + if check_convertible_version(version, error="TorsionDriveResult") == "self": + return self + + dself = self.dict() + if version == 2: + self_vN = qcel.models.v2.TorsionDriveResult(**dself) + + return self_vN + def Optimization(*args, **kwargs): """QC Optimization Results Schema. diff --git a/qcelemental/models/v1/results.py b/qcelemental/models/v1/results.py index ede7197a..386ff2cb 100644 --- a/qcelemental/models/v1/results.py +++ b/qcelemental/models/v1/results.py @@ -2,13 +2,27 @@ from functools import partial from typing import TYPE_CHECKING, Any, Dict, Optional, Set, Union +try: + from typing import Literal +except ImportError: + # remove when minimum py38 + from typing_extensions import Literal + import numpy as np from pydantic.v1 import Field, constr, validator from ...util import provenance_stamp from .basemodels import ProtoModel, qcschema_draft from .basis import BasisSet -from .common_models import ComputeError, DriverEnum, Model, Provenance, qcschema_input_default, qcschema_output_default +from .common_models import ( + ComputeError, + DriverEnum, + Model, + Provenance, + check_convertible_version, + qcschema_input_default, + qcschema_output_default, +) from .molecule import Molecule from .types import Array @@ -567,7 +581,7 @@ class AtomicInput(ProtoModel): f"The QCSchema specification this model conforms to. Explicitly fixed as {qcschema_input_default}." ), ) - schema_version: int = Field( + schema_version: Literal[1] = Field( 1, description="The version number of :attr:`~qcelemental.models.AtomicInput.schema_name` to which this model conforms.", ) @@ -598,6 +612,27 @@ def __repr_args__(self) -> "ReprArgs": ("molecule_hash", self.molecule.get_hash()[:7]), ] + @validator("schema_version", pre=True) + def _version_stamp(cls, v): + # seemingly unneeded, this lets conver_v re-label the model w/o discarding model and + # submodel version fields first. + return 1 + + def convert_v( + self, version: int + ) -> Union["qcelemental.models.v1.AtomicInput", "qcelemental.models.v2.AtomicInput"]: + """Convert to instance of particular QCSchema version.""" + import qcelemental as qcel + + if check_convertible_version(version, error="AtomicInput") == "self": + return self + + dself = self.dict() + if version == 2: + self_vN = qcel.models.v2.AtomicInput(**dself) + + return self_vN + class AtomicResult(AtomicInput): r"""Results from a CMS program execution.""" @@ -608,6 +643,10 @@ class AtomicResult(AtomicInput): f"The QCSchema specification this model conforms to. Explicitly fixed as {qcschema_output_default}." ), ) + schema_version: Literal[1] = Field( + 1, + description="The version number of :attr:`~qcelemental.models.AtomicResult.schema_name` to which this model conforms.", + ) properties: AtomicResultProperties = Field(..., description=str(AtomicResultProperties.__doc__)) wavefunction: Optional[WavefunctionProperties] = Field(None, description=str(WavefunctionProperties.__doc__)) @@ -637,6 +676,10 @@ def _input_to_output(cls, v): "which will be converted to {0}".format(qcschema_output_default, qcschema_input_default) ) + @validator("schema_version", pre=True) + def _version_stamp(cls, v): + return 1 + @validator("return_result") def _validate_return_result(cls, v, values): if values["driver"] == "gradient": @@ -753,6 +796,21 @@ def _native_file_protocol(cls, value, values): ret[rk] = files.get(rk, None) return ret + def convert_v( + self, version: int + ) -> Union["qcelemental.models.v1.AtomicResult", "qcelemental.models.v2.AtomicResult"]: + """Convert to instance of particular QCSchema version.""" + import qcelemental as qcel + + if check_convertible_version(version, error="AtomicResult") == "self": + return self + + dself = self.dict() + if version == 2: + self_vN = qcel.models.v2.AtomicResult(**dself) + + return self_vN + class ResultProperties(AtomicResultProperties): """QC Result Properties Schema. diff --git a/qcelemental/models/v2/common_models.py b/qcelemental/models/v2/common_models.py index da63ba15..93147d9f 100644 --- a/qcelemental/models/v2/common_models.py +++ b/qcelemental/models/v2/common_models.py @@ -127,6 +127,15 @@ def __repr_args__(self) -> "ReprArgs": return [("error", self.error)] +def check_convertible_version(ver: int, error: str): + if ver == 1: + return True + elif ver == 2: + return "self" + else: + raise ValueError(f"QCSchema {error} version={version} does not exist for conversion.") + + qcschema_input_default = "qcschema_input" qcschema_output_default = "qcschema_output" qcschema_optimization_input_default = "qcschema_optimization_input" diff --git a/qcelemental/models/v2/procedures.py b/qcelemental/models/v2/procedures.py index 3e3a365f..9f0ae956 100644 --- a/qcelemental/models/v2/procedures.py +++ b/qcelemental/models/v2/procedures.py @@ -1,5 +1,11 @@ from enum import Enum -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union + +try: + from typing import Literal +except ImportError: + # remove when minimum py38 + from typing_extensions import Literal from pydantic import Field, conlist, constr, field_validator @@ -10,6 +16,7 @@ DriverEnum, Model, Provenance, + check_convertible_version, qcschema_input_default, qcschema_optimization_input_default, qcschema_optimization_output_default, @@ -52,7 +59,7 @@ class QCInputSpecification(ProtoModel): """ schema_name: constr(strip_whitespace=True, pattern=qcschema_input_default) = qcschema_input_default # type: ignore - schema_version: int = 1 + schema_version: int = 1 # TODO driver: DriverEnum = Field(DriverEnum.gradient, description=str(DriverEnum.__doc__)) model: Model = Field(..., description=str(Model.__doc__)) @@ -72,7 +79,7 @@ class OptimizationInput(ProtoModel): schema_name: constr( # type: ignore strip_whitespace=True, pattern=qcschema_optimization_input_default ) = qcschema_optimization_input_default - schema_version: int = 2 + schema_version: Literal[2] = 2 keywords: Dict[str, Any] = Field({}, description="The optimization specific keywords to be used.") extras: Dict[str, Any] = Field({}, description="Extra fields that are not part of the schema.") @@ -89,6 +96,25 @@ def __repr_args__(self) -> "ReprArgs": ("molecule_hash", self.initial_molecule.get_hash()[:7]), ] + @field_validator("schema_version", mode="before") + def _version_stamp(cls, v): + return 2 + + def convert_v( + self, version: int + ) -> Union["qcelemental.models.v1.OptimizationInput", "qcelemental.models.v2.OptimizationInput"]: + """Convert to instance of particular QCSchema version.""" + import qcelemental as qcel + + if check_convertible_version(version, error="OptimizationInput") == "self": + return self + + dself = self.model_dump() + if version == 1: + self_vN = qcel.models.v1.OptimizationInput(**dself) + + return self_vN + class OptimizationResult(OptimizationInput): """QCSchema results model for geometry optimization.""" @@ -96,6 +122,7 @@ class OptimizationResult(OptimizationInput): schema_name: constr( # type: ignore strip_whitespace=True, pattern=qcschema_optimization_output_default ) = qcschema_optimization_output_default + schema_version: Literal[2] = 2 final_molecule: Optional[Molecule] = Field(..., description="The final molecule of the geometry optimization.") trajectory: List[AtomicResult] = Field( @@ -135,6 +162,25 @@ def _trajectory_protocol(cls, v, info): return v + @field_validator("schema_version", mode="before") + def _version_stamp(cls, v): + return 2 + + def convert_v( + self, version: int + ) -> Union["qcelemental.models.v1.OptimizationResult", "qcelemental.models.v2.OptimizationResult"]: + """Convert to instance of particular QCSchema version.""" + import qcelemental as qcel + + if check_convertible_version(version, error="OptimizationResult") == "self": + return self + + dself = self.model_dump() + if version == 1: + self_vN = qcel.models.v1.OptimizationResult(**dself) + + return self_vN + class OptimizationSpecification(ProtoModel): """ @@ -149,7 +195,7 @@ class OptimizationSpecification(ProtoModel): schema_name: constr( strip_whitespace=True, pattern="qcschema_optimization_specification" ) = "qcschema_optimization_specification" # type: ignore - schema_version: int = 1 + schema_version: int = 1 # TODO procedure: str = Field(..., description="Optimization procedure to run the optimization with.") keywords: Dict[str, Any] = Field({}, description="The optimization specific keywords to be used.") @@ -209,7 +255,7 @@ class TorsionDriveInput(ProtoModel): schema_name: constr( strip_whitespace=True, pattern=qcschema_torsion_drive_input_default ) = qcschema_torsion_drive_input_default # type: ignore - schema_version: int = 2 + schema_version: Literal[2] = 2 keywords: TDKeywords = Field(..., description="The torsion drive specific keywords to be used.") extras: Dict[str, Any] = Field({}, description="Extra fields that are not part of the schema.") @@ -231,6 +277,25 @@ def _check_input_specification(cls, value): assert value.driver == DriverEnum.gradient, "driver must be set to gradient" return value + @field_validator("schema_version", mode="before") + def _version_stamp(cls, v): + return 2 + + def convert_v( + self, version: int + ) -> Union["qcelemental.models.v1.TorsionDriveInput", "qcelemental.models.v2.TorsionDriveInput"]: + """Convert to instance of particular QCSchema version.""" + import qcelemental as qcel + + if check_convertible_version(version, error="TorsionDriveInput") == "self": + return self + + dself = self.model_dump() + if version == 1: + self_vN = qcel.models.v1.TorsionDriveInput(**dself) + + return self_vN + class TorsionDriveResult(TorsionDriveInput): """Results from running a torsion drive. @@ -243,7 +308,7 @@ class TorsionDriveResult(TorsionDriveInput): schema_name: constr( strip_whitespace=True, pattern=qcschema_torsion_drive_output_default ) = qcschema_torsion_drive_output_default # type: ignore - schema_version: int = 2 + schema_version: Literal[2] = 2 final_energies: Dict[str, float] = Field( ..., description="The final energy at each angle of the TorsionDrive scan." @@ -265,3 +330,22 @@ class TorsionDriveResult(TorsionDriveInput): ) error: Optional[ComputeError] = Field(None, description=str(ComputeError.__doc__)) provenance: Provenance = Field(..., description=str(Provenance.__doc__)) + + @field_validator("schema_version", mode="before") + def _version_stamp(cls, v): + return 2 + + def convert_v( + self, version: int + ) -> Union["qcelemental.models.v1.TorsionDriveResult", "qcelemental.models.v2.TorsionDriveResult"]: + """Convert to instance of particular QCSchema version.""" + import qcelemental as qcel + + if check_convertible_version(version, error="TorsionDriveResult") == "self": + return self + + dself = self.model_dump() + if version == 1: + self_vN = qcel.models.v1.TorsionDriveResult(**dself) + + return self_vN diff --git a/qcelemental/models/v2/results.py b/qcelemental/models/v2/results.py index 3c787ed1..87c70f98 100644 --- a/qcelemental/models/v2/results.py +++ b/qcelemental/models/v2/results.py @@ -14,7 +14,15 @@ from ...util import provenance_stamp from .basemodels import ExtendedConfigDict, ProtoModel, qcschema_draft from .basis import BasisSet -from .common_models import ComputeError, DriverEnum, Model, Provenance, qcschema_input_default, qcschema_output_default +from .common_models import ( + ComputeError, + DriverEnum, + Model, + Provenance, + check_convertible_version, + qcschema_input_default, + qcschema_output_default, +) from .molecule import Molecule from .types import Array @@ -38,7 +46,7 @@ class AtomicResultProperties(ProtoModel): f"The QCSchema specification this model conforms to. Explicitly fixed as qcschema_atomicproperties." ), ) - schema_version: int = Field( + schema_version: Literal[2] = Field( 2, description="The version number of :attr:`~qcelemental.models.AtomicResultProperties.schema_name` to which this model conforms.", ) @@ -303,6 +311,10 @@ def _validate_derivs(cls, v, info): raise ValueError(f"Derivative must be castable to shape {shape}!") return v + @field_validator("schema_version", mode="before") + def _version_stamp(cls, v): + return 2 + def dict(self, *args, **kwargs): # pure-json dict repr for QCFractal compliance, see https://github.com/MolSSI/QCFractal/issues/579 # Sep 2021: commenting below for now to allow recomposing AtomicResult.properties for qcdb. @@ -658,7 +670,7 @@ class AtomicInput(ProtoModel): f"The QCSchema specification this model conforms to. Explicitly fixed as {qcschema_input_default}." ), ) - schema_version: int = Field( + schema_version: Literal[2] = Field( 2, description="The version number of :attr:`~qcelemental.models.AtomicInput.schema_name` to which this model conforms.", ) @@ -689,6 +701,27 @@ def __repr_args__(self) -> "ReprArgs": ("molecule_hash", self.molecule.get_hash()[:7]), ] + @field_validator("schema_version", mode="before") + def _version_stamp(cls, v): + # seemingly unneeded, this lets conver_v re-label the model w/o discarding model and + # submodel version fields first. + return 2 + + def convert_v( + self, version: int + ) -> Union["qcelemental.models.v1.AtomicInput", "qcelemental.models.v2.AtomicInput"]: + """Convert to instance of particular QCSchema version.""" + import qcelemental as qcel + + if check_convertible_version(version, error="AtomicInput") == "self": + return self + + dself = self.model_dump() + if version == 1: + self_vN = qcel.models.v1.AtomicInput(**dself) + + return self_vN + class AtomicResult(AtomicInput): r"""Results from a CMS program execution.""" @@ -699,6 +732,10 @@ class AtomicResult(AtomicInput): f"The QCSchema specification this model conforms to. Explicitly fixed as {qcschema_output_default}." ), ) + schema_version: Literal[2] = Field( + 2, + description="The version number of :attr:`~qcelemental.models.AtomicResult.schema_name` to which this model conforms.", + ) properties: AtomicResultProperties = Field(..., description=str(AtomicResultProperties.__doc__)) wavefunction: Optional[WavefunctionProperties] = Field(None, description=str(WavefunctionProperties.__doc__)) @@ -729,6 +766,10 @@ def _input_to_output(cls, v): "which will be converted to {0}".format(qcschema_output_default, qcschema_input_default) ) + @field_validator("schema_version", mode="before") + def _version_stamp(cls, v): + return 2 + @field_validator("return_result") @classmethod def _validate_return_result(cls, v, info): @@ -848,3 +889,18 @@ def _native_file_protocol(cls, value, info): for rk in return_keep: ret[rk] = files.get(rk, None) return ret + + def convert_v( + self, version: int + ) -> Union["qcelemental.models.v1.AtomicResult", "qcelemental.models.v2.AtomicResult"]: + """Convert to instance of particular QCSchema version.""" + import qcelemental as qcel + + if check_convertible_version(version, error="AtomicResult") == "self": + return self + + dself = self.model_dump() + if version == 1: + self_vN = qcel.models.v1.AtomicResult(**dself) + + return self_vN From e2079e7e2186703a9f920dcf4bd731f862cfb9df Mon Sep 17 00:00:00 2001 From: "Lori A. Burns" Date: Wed, 25 Sep 2024 12:21:23 -0400 Subject: [PATCH 2/5] add syntax unifiers to v1 --- docs/changelog.rst | 1 + qcelemental/models/v1/basemodels.py | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 201903b9..af2e367f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -35,6 +35,7 @@ New Features Enhancements ++++++++++++ +* v1.ProtoModel learned `model_copy`, `model_dump`, `model_dump_json` methods (all w/o warnings) so downstream can unify on newer syntax. Levi's work alternately/additionally taught v2 `copy`, `dict`, `json` (all w/warning) but dict has an alternate use in Pydantic v2. * ``AtomicInput`` and ``AtomicResult`` ``OptimizationInput``, ``OptimizationResult``, ``TorsionDriveInput``, ``TorsionDriveResult`` (both versions) learned a ``.convert_v(ver)`` function that returns self or the other version. * The ``models.v2`` ``AtomicInput``, ``AtomicResult``, ``AtomicResultProperties`` ``OptimizationInput``, ``OptimizationResult``, ``TorsionDriveInput``, ``TorsionDriveResult`` had their `schema_version` changed to a Literal[2] and validated so new instances will be 2, even if another value passed in. * The ``models.v1`` ``AtomicInput``, ``AtomicResult``, ``OptimizationInput``, ``OptimizationResult``, ``TorsionDriveInput``, ``TorsionDriveResult`` had their `schema_version` changed to a Literal[1] and validated so new instances will be 1, even if another value passed in. diff --git a/qcelemental/models/v1/basemodels.py b/qcelemental/models/v1/basemodels.py index 229b1588..bbcf5d4e 100644 --- a/qcelemental/models/v1/basemodels.py +++ b/qcelemental/models/v1/basemodels.py @@ -118,6 +118,18 @@ def dict(self, **kwargs) -> Dict[str, Any]: else: raise KeyError(f"Unknown encoding type '{encoding}', valid encoding types: 'json'.") + def model_dump(self, **kwargs): + # forwarding pydantic v2 API function to pydantic v1 API so downstream can unify on new syntax + return self.dict(**kwargs) + + def model_dump_json(self, **kwargs): + # forwarding pydantic v2 API function to pydantic v1 API so downstream can unify on new syntax + return self.json(**kwargs) + + def model_copy(self, **kwargs): + # forwarding pydantic v2 API function to pydantic v1 API so downstream can unify on new syntax + return self.copy(**kwargs) + def serialize( self, encoding: str, From 2b0eedb30d563b57beb7599a6740aa4ad649f11c Mon Sep 17 00:00:00 2001 From: "Lori A. Burns" Date: Thu, 26 Sep 2024 19:08:46 -0400 Subject: [PATCH 3/5] convert FailedOp, rearr warnings --- docs/changelog.rst | 3 ++- qcelemental/models/align.py | 5 +++-- qcelemental/models/basemodels.py | 5 +++-- qcelemental/models/basis.py | 5 +++-- qcelemental/models/common_models.py | 7 +++++-- qcelemental/models/molecule.py | 4 ++-- qcelemental/models/procedures.py | 4 ++-- qcelemental/models/results.py | 5 +++-- qcelemental/models/types.py | 4 ++-- qcelemental/models/v1/common_models.py | 15 +++++++++++++++ qcelemental/models/v2/common_models.py | 17 ++++++++++++++++- 11 files changed, 56 insertions(+), 18 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index af2e367f..7a981fa0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -35,8 +35,9 @@ New Features Enhancements ++++++++++++ +* ``v2.FailedOperation`` field `id` is becoming `Optional[str]` instead of plain `str` so that the default validates. * v1.ProtoModel learned `model_copy`, `model_dump`, `model_dump_json` methods (all w/o warnings) so downstream can unify on newer syntax. Levi's work alternately/additionally taught v2 `copy`, `dict`, `json` (all w/warning) but dict has an alternate use in Pydantic v2. -* ``AtomicInput`` and ``AtomicResult`` ``OptimizationInput``, ``OptimizationResult``, ``TorsionDriveInput``, ``TorsionDriveResult`` (both versions) learned a ``.convert_v(ver)`` function that returns self or the other version. +* ``AtomicInput`` and ``AtomicResult`` ``OptimizationInput``, ``OptimizationResult``, ``TorsionDriveInput``, ``TorsionDriveResult``, ``FailedOperation`` (both versions) learned a ``.convert_v(ver)`` function that returns self or the other version. * The ``models.v2`` ``AtomicInput``, ``AtomicResult``, ``AtomicResultProperties`` ``OptimizationInput``, ``OptimizationResult``, ``TorsionDriveInput``, ``TorsionDriveResult`` had their `schema_version` changed to a Literal[2] and validated so new instances will be 2, even if another value passed in. * The ``models.v1`` ``AtomicInput``, ``AtomicResult``, ``OptimizationInput``, ``OptimizationResult``, ``TorsionDriveInput``, ``TorsionDriveResult`` had their `schema_version` changed to a Literal[1] and validated so new instances will be 1, even if another value passed in. * The ``models.v1`` and ``models.v2`` ``OptimizationResult`` given schema_version for the first time. diff --git a/qcelemental/models/align.py b/qcelemental/models/align.py index ca4c17d3..de5370e6 100644 --- a/qcelemental/models/align.py +++ b/qcelemental/models/align.py @@ -2,11 +2,12 @@ import qcelemental +from .common_models import _qcsk_v2_default_v1_importpathschange + _nonapi_file = "align" -_shim_classes_removed_version = "0.40.0" warn( - f"qcelemental.models.{_nonapi_file} should be accessed through qcelemental.models (or qcelemental.models.v1 or .v2 for fixed QCSchema version). The 'models.{_nonapi_file}' route will be removed as soon as v{_shim_classes_removed_version}", + f"qcelemental.models.{_nonapi_file} should be accessed through qcelemental.models (or qcelemental.models.v1 or .v2 for fixed QCSchema version). The 'models.{_nonapi_file}' route will be removed as soon as v{_qcsk_v2_default_v1_importpathschange}.", DeprecationWarning, ) diff --git a/qcelemental/models/basemodels.py b/qcelemental/models/basemodels.py index 2b8da68f..40ed5921 100644 --- a/qcelemental/models/basemodels.py +++ b/qcelemental/models/basemodels.py @@ -2,11 +2,12 @@ import qcelemental +from .common_models import _qcsk_v2_default_v1_importpathschange + _nonapi_file = "basemodels" -_shim_classes_removed_version = "0.40.0" warn( - f"qcelemental.models.{_nonapi_file} should be accessed through qcelemental.models (or qcelemental.models.v1 or .v2 for fixed QCSchema version). The 'models.{_nonapi_file}' route will be removed as soon as v{_shim_classes_removed_version}", + f"qcelemental.models.{_nonapi_file} should be accessed through qcelemental.models (or qcelemental.models.v1 or .v2 for fixed QCSchema version). The 'models.{_nonapi_file}' route will be removed as soon as v{_qcsk_v2_default_v1_importpathschange}.", DeprecationWarning, ) diff --git a/qcelemental/models/basis.py b/qcelemental/models/basis.py index 1acee820..e7a54dcb 100644 --- a/qcelemental/models/basis.py +++ b/qcelemental/models/basis.py @@ -2,11 +2,12 @@ import qcelemental +from .common_models import _qcsk_v2_default_v1_importpathschange + _nonapi_file = "basis" -_shim_classes_removed_version = "0.40.0" warn( - f"qcelemental.models.{_nonapi_file} should be accessed through qcelemental.models (or qcelemental.models.v1 or .v2 for fixed QCSchema version). The 'models.{_nonapi_file}' route will be removed as soon as v{_shim_classes_removed_version}", + f"qcelemental.models.{_nonapi_file} should be accessed through qcelemental.models (or qcelemental.models.v1 or .v2 for fixed QCSchema version). The 'models.{_nonapi_file}' route will be removed as soon as v{_qcsk_v2_default_v1_importpathschange}.", DeprecationWarning, ) diff --git a/qcelemental/models/common_models.py b/qcelemental/models/common_models.py index 666715b3..15c2e098 100644 --- a/qcelemental/models/common_models.py +++ b/qcelemental/models/common_models.py @@ -2,11 +2,14 @@ import qcelemental +_qcsk_v2_available_v1_nochange = "0.50.0" +_qcsk_v2_default_v1_importpathschange = "0.70.0" +_qcsk_v2_nochange_v1_dropped = "1.0.0" + _nonapi_file = "common_models" -_shim_classes_removed_version = "0.40.0" warn( - f"qcelemental.models.{_nonapi_file} should be accessed through qcelemental.models (or qcelemental.models.v1 or .v2 for fixed QCSchema version). The 'models.{_nonapi_file}' route will be removed as soon as v{_shim_classes_removed_version}", + f"qcelemental.models.{_nonapi_file} should be accessed through qcelemental.models (or qcelemental.models.v1 or .v2 for fixed QCSchema version). The 'models.{_nonapi_file}' route will be removed as soon as v{_qcsk_v2_default_v1_importpathschange}.", DeprecationWarning, ) diff --git a/qcelemental/models/molecule.py b/qcelemental/models/molecule.py index c0d62eec..b07a028e 100644 --- a/qcelemental/models/molecule.py +++ b/qcelemental/models/molecule.py @@ -3,10 +3,10 @@ import qcelemental _nonapi_file = "molecule" -_shim_classes_removed_version = "0.40.0" +from .common_models import _qcsk_v2_default_v1_importpathschange warn( - f"qcelemental.models.{_nonapi_file} should be accessed through qcelemental.models (or qcelemental.models.v1 or .v2 for fixed QCSchema version). The 'models.{_nonapi_file}' route will be removed as soon as v{_shim_classes_removed_version}", + f"qcelemental.models.{_nonapi_file} should be accessed through qcelemental.models (or qcelemental.models.v1 or .v2 for fixed QCSchema version). The 'models.{_nonapi_file}' route will be removed as soon as v{_qcsk_v2_default_v1_importpathschange}.", DeprecationWarning, ) diff --git a/qcelemental/models/procedures.py b/qcelemental/models/procedures.py index 91f2bc20..f5084426 100644 --- a/qcelemental/models/procedures.py +++ b/qcelemental/models/procedures.py @@ -3,10 +3,10 @@ import qcelemental _nonapi_file = "procedures" -_shim_classes_removed_version = "0.40.0" +from .common_models import _qcsk_v2_default_v1_importpathschange warn( - f"qcelemental.models.{_nonapi_file} should be accessed through qcelemental.models (or qcelemental.models.v1 or .v2 for fixed QCSchema version). The 'models.{_nonapi_file}' route will be removed as soon as v{_shim_classes_removed_version}", + f"qcelemental.models.{_nonapi_file} should be accessed through qcelemental.models (or qcelemental.models.v1 or .v2 for fixed QCSchema version). The 'models.{_nonapi_file}' route will be removed as soon as v{_qcsk_v2_default_v1_importpathschange}.", DeprecationWarning, ) diff --git a/qcelemental/models/results.py b/qcelemental/models/results.py index 8a6b3916..fee0a7a4 100644 --- a/qcelemental/models/results.py +++ b/qcelemental/models/results.py @@ -2,11 +2,12 @@ import qcelemental +from .common_models import _qcsk_v2_default_v1_importpathschange + _nonapi_file = "results" -_shim_classes_removed_version = "0.40.0" warn( - f"qcelemental.models.{_nonapi_file} should be accessed through qcelemental.models (or qcelemental.models.v1 or .v2 for fixed QCSchema version). The 'models.{_nonapi_file}' route will be removed as soon as v{_shim_classes_removed_version}", + f"qcelemental.models.{_nonapi_file} should be accessed through qcelemental.models (or qcelemental.models.v1 or .v2 for fixed QCSchema version). The 'models.{_nonapi_file}' route will be removed as soon as v{_qcsk_v2_default_v1_importpathschange}.", DeprecationWarning, ) diff --git a/qcelemental/models/types.py b/qcelemental/models/types.py index e94edaf4..346ec5ae 100644 --- a/qcelemental/models/types.py +++ b/qcelemental/models/types.py @@ -3,10 +3,10 @@ import qcelemental _nonapi_file = "types" -_shim_classes_removed_version = "0.40.0" +from .common_models import _qcsk_v2_default_v1_importpathschange warn( - f"qcelemental.models.{_nonapi_file} should be accessed through qcelemental.models (or qcelemental.models.v1 or .v2 for fixed QCSchema version). The 'models.{_nonapi_file}' route will be removed as soon as v{_shim_classes_removed_version}", + f"qcelemental.models.{_nonapi_file} should be accessed through qcelemental.models (or qcelemental.models.v1 or .v2 for fixed QCSchema version). The 'models.{_nonapi_file}' route will be removed as soon as v{_qcsk_v2_default_v1_importpathschange}.", DeprecationWarning, ) diff --git a/qcelemental/models/v1/common_models.py b/qcelemental/models/v1/common_models.py index e27b916a..04ce48d0 100644 --- a/qcelemental/models/v1/common_models.py +++ b/qcelemental/models/v1/common_models.py @@ -128,6 +128,21 @@ class FailedOperation(ProtoModel): def __repr_args__(self) -> "ReprArgs": return [("error", self.error)] + def convert_v( + self, version: int + ) -> Union["qcelemental.models.v1.FailedOperation", "qcelemental.models.v2.FailedOperation"]: + """Convert to instance of particular QCSchema version.""" + import qcelemental as qcel + + if check_convertible_version(version, error="FailedOperation") == "self": + return self + + dself = self.dict() + if version == 2: + self_vN = qcel.models.v2.FailedOperation(**dself) + + return self_vN + def check_convertible_version(ver: int, error: str): if ver == 1: diff --git a/qcelemental/models/v2/common_models.py b/qcelemental/models/v2/common_models.py index 93147d9f..04a94cc8 100644 --- a/qcelemental/models/v2/common_models.py +++ b/qcelemental/models/v2/common_models.py @@ -95,7 +95,7 @@ class FailedOperation(ProtoModel): and containing the reason and input data which generated the failure. """ - id: str = Field( # type: ignore + id: Optional[str] = Field( # type: ignore None, description="A unique identifier which links this FailedOperation, often of the same Id of the operation " "should it have been successful. This will often be set programmatically by a database such as " @@ -126,6 +126,21 @@ class FailedOperation(ProtoModel): def __repr_args__(self) -> "ReprArgs": return [("error", self.error)] + def convert_v( + self, version: int + ) -> Union["qcelemental.models.v1.FailedOperation", "qcelemental.models.v2.FailedOperation"]: + """Convert to instance of particular QCSchema version.""" + import qcelemental as qcel + + if check_convertible_version(version, error="FailedOperation") == "self": + return self + + dself = self.model_dump() + if version == 1: + self_vN = qcel.models.v1.FailedOperation(**dself) + + return self_vN + def check_convertible_version(ver: int, error: str): if ver == 1: From 5e2b0004f201bcc8da46ccbc15caea602eea637c Mon Sep 17 00:00:00 2001 From: "Lori A. Burns" Date: Tue, 1 Oct 2024 02:07:09 -0400 Subject: [PATCH 4/5] np return_result dict --- qcelemental/models/v2/types.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/qcelemental/models/v2/types.py b/qcelemental/models/v2/types.py index 942203a8..27ecb31a 100644 --- a/qcelemental/models/v2/types.py +++ b/qcelemental/models/v2/types.py @@ -11,6 +11,17 @@ def generate_caster(dtype): def cast_to_np(v): + # for driver=properties + if isinstance(v, dict): + vv = {} + for key, val in v.items(): + try: + val = np.asarray(val, dtype=dtype) + except ValueError: + raise ValueError(f"Could not cast {val} to NumPy Array!") + vv[key] = val + return vv + try: v = np.asarray(v, dtype=dtype) except ValueError: From 382947114054d9ce0acb65037626db45b8368c04 Mon Sep 17 00:00:00 2001 From: "Lori A. Burns" Date: Tue, 1 Oct 2024 14:28:06 -0400 Subject: [PATCH 5/5] alt np fix --- qcelemental/models/v2/types.py | 14 +++------- qcelemental/tests/test_model_results.py | 35 +++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/qcelemental/models/v2/types.py b/qcelemental/models/v2/types.py index 27ecb31a..496a13be 100644 --- a/qcelemental/models/v2/types.py +++ b/qcelemental/models/v2/types.py @@ -11,16 +11,10 @@ def generate_caster(dtype): def cast_to_np(v): - # for driver=properties - if isinstance(v, dict): - vv = {} - for key, val in v.items(): - try: - val = np.asarray(val, dtype=dtype) - except ValueError: - raise ValueError(f"Could not cast {val} to NumPy Array!") - vv[key] = val - return vv + if isinstance(v, (float, dict)): + return v + elif isinstance(v, int): + return float(v) try: v = np.asarray(v, dtype=dtype) diff --git a/qcelemental/tests/test_model_results.py b/qcelemental/tests/test_model_results.py index 395d9ce2..f02928cf 100644 --- a/qcelemental/tests/test_model_results.py +++ b/qcelemental/tests/test_model_results.py @@ -1,3 +1,6 @@ +import copy +import warnings + import numpy as np import pydantic import pytest @@ -618,3 +621,35 @@ def test_result_model_deprecations(result_data_fixture, optimization_data_fixtur with pytest.warns(DeprecationWarning): qcel.models.v1.Optimization(**optimization_data_fixture) + + +@pytest.mark.parametrize( + "retres,atprop,rettyp", + [ + (15, "mp2_correlation_energy", float), + (15.0, "mp2_correlation_energy", float), + ([1.0, -2.5, 3, 0, 0, 0, 0, 0, 0], "return_gradient", np.ndarray), + (np.array([1.0, -2.5, 3, 0, 0, 0, 0, 0, 0]), "return_gradient", np.ndarray), + ({"cat1": "tail", "cat2": "whiskers"}, None, str), + ({"float1": 4.4, "float2": -9.9}, None, float), + ({"list1": [-1.0, 4.4], "list2": [-9.9, 2]}, None, list), + ({"arr1": np.array([-1.0, 4.4]), "arr2": np.array([-9.9, 2])}, None, np.ndarray), + ], +) +def test_return_result_types(result_data_fixture, retres, atprop, rettyp, request, schema_versions): + AtomicResult = schema_versions.AtomicResult + + working_res = copy.deepcopy(result_data_fixture) + working_res["return_result"] = retres + if atprop: + working_res["properties"]["calcinfo_natom"] = 3 + working_res["properties"][atprop] = retres + atres = AtomicResult(**working_res) + + if isinstance(retres, dict): + for v in atres.return_result.values(): + assert isinstance(v, rettyp) + else: + if atprop: + assert isinstance(getattr(atres.properties, atprop), rettyp) + assert isinstance(atres.return_result, rettyp)