diff --git a/qcelemental/models/__init__.py b/qcelemental/models/__init__.py index 4a69f39a..33375580 100644 --- a/qcelemental/models/__init__.py +++ b/qcelemental/models/__init__.py @@ -8,14 +8,12 @@ from . import types from .align import AlignmentMill -from .basemodels import AutodocBaseSettings, ProtoModel +from .basemodels import AutodocBaseSettings, ProtoModel, Provenance from .basis import BasisSet -from .common_models import ComputeError, DriverEnum, FailedOperation, Provenance +from .common_models import ComputeError, DriverEnum from .molecule import Molecule -from .procedures import OptimizationInput, OptimizationResult -from .procedures import Optimization # scheduled for removal +from .procedures import OptimizationInput, OptimizationResult, FailedOperation, TorsionDriveInput, TorsionDriveResult from .results import AtomicInput, AtomicResult, AtomicResultProperties -from .results import Result, ResultInput, ResultProperties # scheduled for removal def qcschema_models(): diff --git a/qcelemental/models/abcmodels.py b/qcelemental/models/abcmodels.py new file mode 100644 index 00000000..5a0946bb --- /dev/null +++ b/qcelemental/models/abcmodels.py @@ -0,0 +1,91 @@ +import abc +import json +from pathlib import Path +from typing import Any, Dict, Optional, Set, Union + +import numpy as np +from pydantic import BaseModel, BaseSettings, Field +from typing_extensions import Literal + +from qcelemental.testing import compare_recursive +from qcelemental.util import deserialize, serialize +from qcelemental.util.autodocs import AutoPydanticDocGenerator + +from ..util import provenance_stamp +from .basemodels import ProtoModel, Provenance +from .molecule import Molecule + + +class InputResultBase(ProtoModel, abc.ABC): + """Base class from which input and result models are derived + + + NOTE: Abstract implementation coming from Samuel Colvin's suggestion: + https://github.com/samuelcolvin/pydantic/discussions/2410#discussioncomment-408613 + """ + + @property + @abc.abstractmethod + def schema_name(self) -> str: + """The QCSchema specification this model conforms to""" + + schema_version: Literal[2] = Field( + 2, description="The version number of ``schema_name`` to which this model conforms." + ) + id: Optional[str] = Field(None, description="The optional ID for the computation.") + extras: Dict[str, Any] = Field( + {}, + description="Additional information to bundle with the computation. Use for schema development and scratch space.", + ) + + provenance: Provenance = Field(..., description=str(Provenance.__doc__)) + + +class InputBase(InputResultBase): + """Base Class for all input objects""" + + provenance: Provenance = Field(Provenance(**provenance_stamp(__name__)), description=str(Provenance.__doc__)) + + +class ResultBase(InputResultBase): + """Base class for all result classes""" + + input_data: Any = Field(..., description="The input data supplied to generate the computation") + success: bool = Field(..., description="The success of program execution") + + stdout: Optional[str] = Field( + None, + description="The primary logging output of the program, whether natively standard output or a file. Presence vs. absence (or null-ness?) configurable by protocol.", + ) + stderr: Optional[str] = Field(None, description="The standard error of the program execution.") + + +class SuccessfulResultBase(ResultBase): + """Base object for any successfully returned result""" + + success: Literal[True] = Field( + True, + description="A boolean indicator that the operation succeeded consistent with the model of successful operations. " + "Should always be True. Allows programmatic assessment of all operations regardless of if they failed or " + "succeeded", + ) + + +class InputSpecificationBase(InputBase): + """Input specification base""" + + keywords: Dict[str, Any] = Field({}, description="The program specific keywords to be used.") + protocols: Any = Field(..., description="Protocols associated with the input") + + +class InputComputationBase(InputBase): + """Base input directed at any computational chemistry program""" + + input_spec: InputSpecificationBase = Field(..., description="The input specification for the computation") + molecule: Molecule = Field(..., description="The molecule to use in the computation.") + + @property + def initial_molecule(self) -> Molecule: + """To maintain backwards compatibility to access the 'initial_molecule' attribute""" + # NOTE: Useful? Still have to use 'molecule' for instantiating the object... + return self.molecule diff --git a/qcelemental/models/basemodels.py b/qcelemental/models/basemodels.py index 5b96042c..6e83cd15 100644 --- a/qcelemental/models/basemodels.py +++ b/qcelemental/models/basemodels.py @@ -1,14 +1,17 @@ +import abc import json from pathlib import Path from typing import Any, Dict, Optional, Set, Union import numpy as np -from pydantic import BaseModel, BaseSettings +from pydantic import BaseModel, BaseSettings, Field from qcelemental.testing import compare_recursive from qcelemental.util import deserialize, serialize from qcelemental.util.autodocs import AutoPydanticDocGenerator +from ..util import provenance_stamp + def _repr(self) -> str: return f'{self.__repr_name__()}({self.__repr_str__(", ")})' @@ -189,6 +192,24 @@ def compare(self, other: Union["ProtoModel", BaseModel], **kwargs) -> bool: return compare_recursive(self, other, **kwargs) +class Provenance(ProtoModel): + """Provenance information.""" + + creator: str = Field(..., description="The name of the program, library, or person who created the object.") + version: str = Field( + "", + description="The version of the creator, blank otherwise. This should be sortable by the very broad [PEP 440](https://www.python.org/dev/peps/pep-0440/).", + ) + routine: str = Field("", description="The name of the routine or function within the creator, blank otherwise.") + + class Config(ProtoModel.Config): + canonical_repr = True + extra: str = "allow" + + def schema_extra(schema, model): + schema["$schema"] = qcschema_draft + + class AutodocBaseSettings(BaseSettings): def __init_subclass__(cls) -> None: cls.__doc__ = AutoPydanticDocGenerator(cls, always_apply=True) diff --git a/qcelemental/models/common_models.py b/qcelemental/models/common_models.py index 226b4158..d67e1ea6 100644 --- a/qcelemental/models/common_models.py +++ b/qcelemental/models/common_models.py @@ -15,24 +15,6 @@ ndarray_encoder = {np.ndarray: lambda v: v.flatten().tolist()} -class Provenance(ProtoModel): - """Provenance information.""" - - creator: str = Field(..., description="The name of the program, library, or person who created the object.") - version: str = Field( - "", - description="The version of the creator, blank otherwise. This should be sortable by the very broad [PEP 440](https://www.python.org/dev/peps/pep-0440/).", - ) - routine: str = Field("", description="The name of the routine or function within the creator, blank otherwise.") - - class Config(ProtoModel.Config): - canonical_repr = True - extra: str = "allow" - - def schema_extra(schema, model): - schema["$schema"] = qcschema_draft - - class Model(ProtoModel): """The computational molecular sciences model to run.""" @@ -94,45 +76,13 @@ def __repr_args__(self) -> "ReprArgs": return [("error_type", self.error_type), ("error_message", self.error_message)] -class FailedOperation(ProtoModel): - """Record indicating that a given operation (program, procedure, etc.) has failed and containing the reason and input data which generated the failure.""" - - id: 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 " - "Fractal.", - ) - input_data: Any = Field( # type: ignore - None, - description="The input data which was passed in that generated this failure. This should be the complete " - "input which when attempted to be run, caused the operation to fail.", - ) - success: bool = Field( # type: ignore - False, - description="A boolean indicator that the operation failed consistent with the model of successful operations. " - "Should always be False. Allows programmatic assessment of all operations regardless of if they failed or " - "succeeded", - ) - error: ComputeError = Field( # type: ignore - ..., - description="A container which has details of the error that failed this operation. See the " - ":class:`ComputeError` for more details.", - ) - extras: Optional[Dict[str, Any]] = Field( # type: ignore - None, - description="Additional information to bundle with the failed operation. Details which pertain specifically " - "to a thrown error should be contained in the `error` field. See :class:`ComputeError` for details.", - ) - - def __repr_args__(self) -> "ReprArgs": - return [("error", self.error)] - - qcschema_input_default = "qcschema_input" qcschema_output_default = "qcschema_output" +qcschema_input_specification_default = "qcschema_input_specification" +qcschema_optimization_specification_default = "qcschema_optimization_specification" qcschema_optimization_input_default = "qcschema_optimization_input" qcschema_optimization_output_default = "qcschema_optimization_output" qcschema_torsion_drive_input_default = "qcschema_torsion_drive_input" qcschema_torsion_drive_output_default = "qcschema_torsion_drive_output" -qcschema_molecule_default = "qcschema_molecule" +qcschema_torsion_drive_specification_default = "qcschema_torsion_drive_specification" +qcschema_molecule_default = "qcschema_molecule" \ No newline at end of file diff --git a/qcelemental/models/molecule.py b/qcelemental/models/molecule.py index 2fe7b2b3..abe7cfb1 100644 --- a/qcelemental/models/molecule.py +++ b/qcelemental/models/molecule.py @@ -22,8 +22,8 @@ from ..physical_constants import constants from ..testing import compare, compare_values from ..util import deserialize, measure_coordinates, msgpackext_loads, provenance_stamp, which_import -from .basemodels import ProtoModel, qcschema_draft -from .common_models import Provenance, qcschema_molecule_default +from .basemodels import ProtoModel, Provenance, qcschema_draft +from .common_models import qcschema_molecule_default from .types import Array if TYPE_CHECKING: @@ -113,12 +113,7 @@ class Molecule(ProtoModel): """ - schema_name: constr(strip_whitespace=True, regex="^(qcschema_molecule)$") = Field( # type: ignore - qcschema_molecule_default, - description=( - f"The QCSchema specification to which this model conforms. Explicitly fixed as {qcschema_molecule_default}." - ), - ) + schema_name: constr(strip_whitespace=True, regex=qcschema_molecule_default) = qcschema_molecule_default # type: ignore schema_version: int = Field( # type: ignore 2, description="The version number of ``schema_name`` to which this model conforms." ) diff --git a/qcelemental/models/procedures.py b/qcelemental/models/procedures.py index 50b763f9..e7310a9f 100644 --- a/qcelemental/models/procedures.py +++ b/qcelemental/models/procedures.py @@ -1,23 +1,31 @@ +from asyncio import protocols from enum import Enum -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple +from typing import TYPE_CHECKING, ClassVar, Dict, List, Optional, Tuple, Union -from pydantic import Field, constr, validator, conlist +from pydantic import Field, constr, validator +from typing_extensions import Literal -from ..util import provenance_stamp +from .abcmodels import ResultBase from .basemodels import ProtoModel from .common_models import ( ComputeError, DriverEnum, - Model, - Provenance, - qcschema_input_default, qcschema_optimization_input_default, qcschema_optimization_output_default, + qcschema_optimization_specification_default, qcschema_torsion_drive_input_default, qcschema_torsion_drive_output_default, + qcschema_torsion_drive_specification_default, ) from .molecule import Molecule -from .results import AtomicResult +from .results import ( + AtomicInput, + AtomicResult, + InputComputationBase, + InputSpecificationBase, + QCInputSpecification, + SuccessfulResultBase, +) if TYPE_CHECKING: from pydantic.typing import ReprArgs @@ -47,76 +55,71 @@ class Config: force_skip_defaults = True -class QCInputSpecification(ProtoModel): - """ - A compute description for energy, gradient, and Hessian computations used in a geometry optimization. +class OptimizationSpecification(InputSpecificationBase): """ + A specification for how a geometry optimization should be performed **inside** of + another procedure. - schema_name: constr(strip_whitespace=True, regex=qcschema_input_default) = qcschema_input_default # type: ignore - schema_version: int = 1 - - driver: DriverEnum = Field(DriverEnum.gradient, description=str(DriverEnum.__doc__)) - model: Model = Field(..., description=str(Model.__doc__)) - keywords: Dict[str, Any] = Field({}, description="The program specific keywords to be used.") + Notes + ----- + * This class is still provisional and may be subject to removal and re-design. + * NOTE: I suggest this object be used analogous to QCInputSpecification but for optimizations + """ - extras: Dict[str, Any] = Field( - {}, - description="Additional information to bundle with the computation. Use for schema development and scratch space.", - ) + # schema_name: constr(strip_whitespace=True, regex=qcschema_optimization_specification) = qcschema_optimization_specification # type: ignore + schema_name: ClassVar[str] = qcschema_optimization_specification_default + protocols: OptimizationProtocols = Field(OptimizationProtocols(), description=str(OptimizationProtocols.__doc__)) + # NOTE: Need a little help knowing how procedure field is used. What values might it contain? + procedure: Optional[str] = Field(None, description="Optimization procedure to run the optimization with.") + @validator("procedure") + def _check_procedure(cls, v): + return v.lower() -class OptimizationInput(ProtoModel): - id: Optional[str] = None - hash_index: Optional[str] = None - schema_name: constr( # type: ignore - strip_whitespace=True, regex=qcschema_optimization_input_default - ) = qcschema_optimization_input_default - schema_version: int = 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.") - protocols: OptimizationProtocols = Field(OptimizationProtocols(), description=str(OptimizationProtocols.__doc__)) +class OptimizationInput(InputComputationBase): + """Input object for an optimization computation""" - input_specification: QCInputSpecification = Field(..., description=str(QCInputSpecification.__doc__)) - initial_molecule: Molecule = Field(..., description="The starting molecule for the geometry optimization.") + schema_name: ClassVar[str] = qcschema_optimization_input_default + hash_index: Optional[str] = None # NOTE: Need this field? + input_spec: OptimizationSpecification = Field( + OptimizationSpecification(), description=OptimizationSpecification.__doc__ + ) + gradient_spec: QCInputSpecification = Field(..., description=str(QCInputSpecification.__doc__)) - provenance: Provenance = Field(Provenance(**provenance_stamp(__name__)), description=str(Provenance.__doc__)) + @validator("gradient_spec") + def _check_gradient_spec(cls, value): + assert value.driver == DriverEnum.gradient, "driver must be set to gradient" + return value def __repr_args__(self) -> "ReprArgs": return [ - ("model", self.input_specification.model.dict()), - ("molecule_hash", self.initial_molecule.get_hash()[:7]), + ("model", self.gradient_spec.model.dict()), + ("molecule_hash", self.molecule.get_hash()[:7]), ] -class OptimizationResult(OptimizationInput): - schema_name: constr( # type: ignore - strip_whitespace=True, regex=qcschema_optimization_output_default - ) = qcschema_optimization_output_default +class OptimizationResult(SuccessfulResultBase): + """The result of an optimization procedure""" + schema_name: ClassVar[str] = qcschema_optimization_output_default + # schema_name: constr(strip_whitespace=True, regex=qcschema_optimization_output_default) = qcschema_optimization_output_default # type: ignore + input_data: OptimizationInput = Field(..., description=str(OptimizationInput.__doc__)) + # NOTE: If Optional we want None instead of ...; is there a reason for ...? Should the attribute not be Optional? final_molecule: Optional[Molecule] = Field(..., description="The final molecule of the geometry optimization.") trajectory: List[AtomicResult] = Field( ..., description="A list of ordered Result objects for each step in the optimization." ) energies: List[float] = Field(..., description="A list of ordered energies for each step in the optimization.") - stdout: Optional[str] = Field(None, description="The standard output of the program.") - stderr: Optional[str] = Field(None, description="The standard error of the program.") - - success: bool = Field( - ..., description="The success of a given programs execution. If False, other fields may be blank." - ) - error: Optional[ComputeError] = Field(None, description=str(ComputeError.__doc__)) - provenance: Provenance = Field(..., description=str(Provenance.__doc__)) - @validator("trajectory", each_item=False) def _trajectory_protocol(cls, v, values): + # NOTE: Commenting out because with current setup field is gauranteed to always exist + # Do not propagate validation errors + # if "protocols" not in values["input_data"]: + # raise ValueError("Protocols was not properly formed.") - # Do not propogate validation errors - if "protocols" not in values: - raise ValueError("Protocols was not properly formed.") - - keep_enum = values["protocols"].trajectory + keep_enum = values["input_data"].input_spec.protocols.trajectory if keep_enum == "all": pass elif keep_enum == "initial_and_final": @@ -133,28 +136,6 @@ def _trajectory_protocol(cls, v, values): return v -class OptimizationSpecification(ProtoModel): - """ - A specification for how a geometry optimization should be performed **inside** of - another procedure. - - Notes - ----- - * This class is still provisional and may be subject to removal and re-design. - """ - - schema_name: constr(strip_whitespace=True, regex="qcschema_optimization_specification") = "qcschema_optimization_specification" # type: ignore - schema_version: int = 1 - - procedure: str = Field(..., description="Optimization procedure to run the optimization with.") - keywords: Dict[str, Any] = Field({}, description="The optimization specific keywords to be used.") - protocols: OptimizationProtocols = Field(OptimizationProtocols(), description=str(OptimizationProtocols.__doc__)) - - @validator("procedure") - def _check_procedure(cls, v): - return v.lower() - - class TDKeywords(ProtoModel): """ TorsionDriveRecord options @@ -192,7 +173,16 @@ class TDKeywords(ProtoModel): ) -class TorsionDriveInput(ProtoModel): +class TorsionDriveSpecification(InputSpecificationBase): + """Specification for a Torsion Drive computation""" + + protocols: None = None + schema_name: ClassVar[str] = qcschema_torsion_drive_specification_default + # schema_name: constr(strip_whitespace=True, regex=qcschema_torsion_drive_input_default) = qcschema_torsion_drive_input_default # type: ignore + keywords: TDKeywords = Field(..., description="The torsion drive specific keywords to be used.") + + +class TorsionDriveInput(InputComputationBase): """Inputs for running a torsion drive. Notes @@ -200,30 +190,20 @@ class TorsionDriveInput(ProtoModel): * This class is still provisional and may be subject to removal and re-design. """ - schema_name: constr(strip_whitespace=True, regex=qcschema_torsion_drive_input_default) = qcschema_torsion_drive_input_default # type: ignore - schema_version: int = 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.") - - input_specification: QCInputSpecification = Field(..., description=str(QCInputSpecification.__doc__)) - initial_molecule: conlist(item_type=Molecule, min_items=1) = Field( - ..., description="The starting molecule(s) for the torsion drive." - ) - + schema_name: ClassVar[str] = qcschema_torsion_drive_input_default + input_spec: TorsionDriveSpecification = Field(..., description=(str(TorsionDriveSpecification.__doc__))) + gradient_spec: QCInputSpecification = Field(..., description=str(QCInputSpecification.__doc__)) optimization_spec: OptimizationSpecification = Field( - ..., description="Settings to use for optimizations at each grid angle." + OptimizationSpecification(), description="Settings to use for optimizations at each grid angle." ) - provenance: Provenance = Field(Provenance(**provenance_stamp(__name__)), description=str(Provenance.__doc__)) - - @validator("input_specification") - def _check_input_specification(cls, value): + @validator("gradient_spec") + def _check_gradient_spec(cls, value): assert value.driver == DriverEnum.gradient, "driver must be set to gradient" return value -class TorsionDriveResult(TorsionDriveInput): +class TorsionDriveResult(SuccessfulResultBase): """Results from running a torsion drive. Notes @@ -231,33 +211,37 @@ class TorsionDriveResult(TorsionDriveInput): * This class is still provisional and may be subject to removal and re-design. """ - schema_name: constr(strip_whitespace=True, regex=qcschema_torsion_drive_output_default) = qcschema_torsion_drive_output_default # type: ignore - schema_version: int = 1 - + schema_name: ClassVar[str] = qcschema_torsion_drive_output_default + input_data: TorsionDriveInput = Field(..., description="TorsionDriveInput used to generate the computation") final_energies: Dict[str, float] = Field( ..., description="The final energy at each angle of the TorsionDrive scan." ) final_molecules: Dict[str, Molecule] = Field( ..., description="The final molecule at each angle of the TorsionDrive scan." ) - optimization_history: Dict[str, List[OptimizationResult]] = Field( ..., description="The map of each angle of the TorsionDrive scan to each optimization computations.", ) - stdout: Optional[str] = Field(None, description="The standard output of the program.") - stderr: Optional[str] = Field(None, description="The standard error of the program.") - - success: bool = Field( - ..., description="The success of a given programs execution. If False, other fields may be blank." - ) - error: Optional[ComputeError] = Field(None, description=str(ComputeError.__doc__)) - provenance: Provenance = Field(..., description=str(Provenance.__doc__)) +class FailedOperation(ResultBase): + """Record indicating that a given operation (program, procedure, etc.) has failed and containing the reason and input data which generated the failure.""" -def Optimization(*args, **kwargs): - from warnings import warn + input_data: Union[AtomicInput, OptimizationInput, TorsionDriveInput] = Field( + ..., description="The input data supplied to generate this computation" + ) + success: Literal[False] = Field( # type: ignore + False, + description="A boolean indicator that the operation failed consistent with the model of successful operations. " + "Should always be False. Allows programmatic assessment of all operations regardless of if they failed or " + "succeeded", + ) + error: ComputeError = Field( # type: ignore + ..., + description="A container which has details of the error that failed this operation. See the " + ":class:`ComputeError` for more details.", + ) - warn("Optimization has been renamed to OptimizationResult and will be removed in v0.13.0", DeprecationWarning) - return OptimizationResult(*args, **kwargs) + def __repr_args__(self) -> "ReprArgs": + return [("error", self.error)] diff --git a/qcelemental/models/results.py b/qcelemental/models/results.py index d1497a94..c50b583e 100644 --- a/qcelemental/models/results.py +++ b/qcelemental/models/results.py @@ -1,15 +1,19 @@ from enum import Enum -from functools import partial -from typing import TYPE_CHECKING, Any, Dict, Optional, Set, Union +from typing import TYPE_CHECKING, Any, ClassVar, Dict, Optional, Set, Union import numpy as np from pydantic import Field, constr, validator -from ..util import provenance_stamp +from .abcmodels import InputComputationBase, InputSpecificationBase, SuccessfulResultBase 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 .molecule import Molecule +from .common_models import ( + DriverEnum, + Model, + qcschema_input_default, + qcschema_input_specification_default, + qcschema_output_default, +) from .types import Array if TYPE_CHECKING: @@ -505,6 +509,14 @@ class WavefunctionProtocolEnum(str, Enum): none = "none" +class NativeFilesProtocolEnum(str, Enum): + r"""CMS program files to keep from a computation.""" + + all = "all" + input = "input" + none = "none" + + class ErrorCorrectionProtocol(ProtoModel): r"""Configuration for how QCEngine handles error correction @@ -527,14 +539,6 @@ def allows(self, policy: str): return self.policies.get(policy, self.default_policy) -class NativeFilesProtocolEnum(str, Enum): - r"""CMS program files to keep from a computation.""" - - all = "all" - input = "input" - none = "none" - - class AtomicResultProtocols(ProtoModel): r"""Protocols regarding the manipulation of computational result data.""" @@ -557,34 +561,22 @@ class Config: ### Primary models -class AtomicInput(ProtoModel): - r"""The MolSSI Quantum Chemistry Schema""" +class QCInputSpecification(InputSpecificationBase): + """Input specification for single point QC calculations""" - id: Optional[str] = Field(None, description="The optional ID for the computation.") - schema_name: constr(strip_whitespace=True, regex="^(qc_?schema_input)$") = Field( # type: ignore - qcschema_input_default, - description=( - f"The QCSchema specification this model conforms to. Explicitly fixed as {qcschema_input_default}." - ), - ) - schema_version: int = Field(1, description="The version number of ``schema_name`` to which this model conforms.") - - molecule: Molecule = Field(..., description="The molecule to use in the computation.") + schema_name: ClassVar[str] = qcschema_input_specification_default driver: DriverEnum = Field(..., description=str(DriverEnum.__doc__)) - model: Model = Field(..., description=str(Model.__base_doc__)) - keywords: Dict[str, Any] = Field({}, description="The program-specific keywords to be used.") + model: Model = Field(..., description=str(Model.__doc__)) protocols: AtomicResultProtocols = Field( AtomicResultProtocols(), description=str(AtomicResultProtocols.__base_doc__) ) - extras: Dict[str, Any] = Field( - {}, - description="Additional information to bundle with the computation. Use for schema development and scratch space.", - ) - provenance: Provenance = Field( - default_factory=partial(provenance_stamp, __name__), description=str(Provenance.__base_doc__) - ) +class AtomicInput(InputComputationBase): + """The MolSSI Quantum Chemistry Schema""" + + schema_name: ClassVar[str] = qcschema_input_default + input_spec: QCInputSpecification = Field(..., description=str(QCInputSpecification.__doc__)) class Config(ProtoModel.Config): def schema_extra(schema, model): @@ -592,21 +584,17 @@ def schema_extra(schema, model): def __repr_args__(self) -> "ReprArgs": return [ - ("driver", self.driver.value), - ("model", self.model.dict()), + ("driver", self.input_spec.driver.value), + ("model", self.input_spec.model.dict()), ("molecule_hash", self.molecule.get_hash()[:7]), ] -class AtomicResult(AtomicInput): +class AtomicResult(SuccessfulResultBase): r"""Results from a CMS program execution.""" - - schema_name: constr(strip_whitespace=True, regex="^(qc_?schema_output)$") = Field( # type: ignore - qcschema_output_default, - description=( - f"The QCSchema specification this model conforms to. Explicitly fixed as {qcschema_output_default}." - ), - ) + # schema_name: constr(strip_whitespace=True, regex=qcschema_output_default) = qcschema_output_default # type: ignore + schema_name: ClassVar[str] = qcschema_output_default + input_data: AtomicInput = Field(..., description="The input data supplied to generate this computation") properties: AtomicResultProperties = Field(..., description=str(AtomicResultProperties.__base_doc__)) wavefunction: Optional[WavefunctionProperties] = Field(None, description=str(WavefunctionProperties.__base_doc__)) @@ -615,34 +603,16 @@ class AtomicResult(AtomicInput): description="The primary return specified by the ``driver`` field. Scalar if energy; array if gradient or hessian; dictionary with property keys if properties.", ) # type: ignore - stdout: Optional[str] = Field( - None, - description="The primary logging output of the program, whether natively standard output or a file. Presence vs. absence (or null-ness?) configurable by protocol.", - ) - stderr: Optional[str] = Field(None, description="The standard error of the program execution.") - native_files: Optional[Dict[str, Any]] = Field(None, description="DSL files.") - - success: bool = Field(..., description="The success of program execution. If False, other fields may be blank.") - error: Optional[ComputeError] = Field(None, description=str(ComputeError.__base_doc__)) - provenance: Provenance = Field(..., description=str(Provenance.__base_doc__)) - - @validator("schema_name", pre=True) - def _input_to_output(cls, v): - r"""If qcschema_input is passed in, cast it to output, otherwise no""" - if v.lower().strip() in [qcschema_input_default, qcschema_output_default]: - return qcschema_output_default - raise ValueError( - "Only {0} or {1} is allowed for schema_name, " - "which will be converted to {0}".format(qcschema_output_default, qcschema_input_default) - ) + native_files: Dict[str, Any] = Field({}, description="DSL files.") @validator("return_result") def _validate_return_result(cls, v, values): - if values["driver"] == "gradient": + driver = values["input_data"].input_spec.driver + if driver == "gradient": v = np.asarray(v).reshape(-1, 3) - elif values["driver"] == "hessian": + elif driver == "hessian": v = np.asarray(v) - nsq = int(v.size ** 0.5) + nsq = int(v.size**0.5) v.shape = (nsq, nsq) return v @@ -720,10 +690,10 @@ def _wavefunction_protocol(cls, value, values): def _stdout_protocol(cls, value, values): # Do not propagate validation errors - if "protocols" not in values: + if "protocols" not in values["input_spec"]: raise ValueError("Protocols was not properly formed.") - outp = values["protocols"].stdout + outp = values["input_spec"]["protocols"].stdout if outp is True: return value elif outp is False: @@ -752,41 +722,3 @@ def _native_file_protocol(cls, value, values): for rk in return_keep: ret[rk] = files.get(rk, None) return ret - - -class ResultProperties(AtomicResultProperties): - def __init__(self, *args, **kwargs): - from warnings import warn - - warn( - "ResultProperties has been renamed to AtomicResultProperties and will be removed in v0.13.0", - DeprecationWarning, - ) - super().__init__(*args, **kwargs) - - -class ResultProtocols(AtomicResultProtocols): - def __init__(self, *args, **kwargs): - from warnings import warn - - warn( - "ResultProtocols has been renamed to AtomicResultProtocols and will be removed in v0.13.0", - DeprecationWarning, - ) - super().__init__(*args, **kwargs) - - -class ResultInput(AtomicInput): - def __init__(self, *args, **kwargs): - from warnings import warn - - warn("ResultInput has been renamed to AtomicInput and will be removed in v0.13.0", DeprecationWarning) - super().__init__(*args, **kwargs) - - -class Result(AtomicResult): - def __init__(self, *args, **kwargs): - from warnings import warn - - warn("Result has been renamed to AtomicResult and will be removed in v0.13.0", DeprecationWarning) - super().__init__(*args, **kwargs)