Skip to content

Commit

Permalink
Feature normalize and standardize objects (#287)
Browse files Browse the repository at this point in the history
* Refactored models. Moved common fields to parent classes. Created standardized input and result interface.

* Basic models implemented; tests not passing yet

* test_model_results passing

* test_molutil passing

* test_molparse_from_string_passing

* test_molecule passing

* Set test_molutil back to original state

* blacken qcel

* Skip --validate tests since they are circular in nature. They test that exported models conform to pydantic's autogenerated schema, which is not necessary to tests. Also, issues arrise with jsonschema which are external to our concerns.
  • Loading branch information
coltonbh authored Jul 21, 2022
1 parent 2956421 commit 6e341fd
Show file tree
Hide file tree
Showing 13 changed files with 336 additions and 435 deletions.
13 changes: 7 additions & 6 deletions qcelemental/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@

from . import types
from .align import AlignmentMill
from .basemodels import AutodocBaseSettings # remove when QCFractal merges `next`
from .basemodels import ProtoModel
from .basemodels import (
AutodocBaseSettings,
ProtoModel,
Provenance,
) # remove AutodocBaseSettings when QCFractal merges `next`
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():
Expand Down
23 changes: 20 additions & 3 deletions qcelemental/models/basemodels.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
from typing import Any, Dict, Optional, Set, Union

import numpy as np
from pydantic import BaseSettings # remove when QCFractal merges `next`
from pydantic import BaseModel
from pydantic import BaseModel, BaseSettings, Field # remove BaseSettings when QCFractal merges `next`

from qcelemental.util import deserialize, serialize
from qcelemental.util.autodocs import AutoPydanticDocGenerator # remove when QCFractal merges `next`
Expand Down Expand Up @@ -191,8 +190,26 @@ def compare(self, other: Union["ProtoModel", BaseModel], **kwargs) -> bool:
return compare_recursive(self, other, **kwargs)


# remove when QCFractal merges `next`
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):
# remove when QCFractal merges `next`
def __init_subclass__(cls) -> None:
cls.__doc__ = AutoPydanticDocGenerator(cls, always_apply=True)

Expand Down
64 changes: 1 addition & 63 deletions qcelemental/models/common_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import numpy as np
from pydantic import Field

from .basemodels import ProtoModel, qcschema_draft
from .basemodels import ProtoModel
from .basis import BasisSet

if TYPE_CHECKING:
Expand All @@ -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."""

Expand Down Expand Up @@ -92,47 +74,3 @@ class Config:

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_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"
45 changes: 45 additions & 0 deletions qcelemental/models/inputresult_abc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from typing import Any, Dict, Optional

from pydantic import Field
from typing_extensions import Literal


from .qcschema_abc import AutoSetProvenance, QCSchemaModelBase
from .molecule import Molecule


class SpecificationBase(AutoSetProvenance):
"""Specification objects contain the keywords and other configurable parameters directed at a particular QC program"""

keywords: Dict[str, Any] = Field({}, description="The program specific keywords to be used.")
program: str = Field(..., description="The program for which the Specification is intended.")


class InputBase(AutoSetProvenance):
"""An Input is composed of a .specification and a .molecule which together fully specify a computation"""

specification: SpecificationBase = Field(..., description=SpecificationBase.__doc__)
molecule: Molecule = Field(..., description=Molecule.__doc__)


class ResultBase(QCSchemaModelBase):
"""Base class for all result classes"""

input_data: InputBase = Field(..., description=InputBase.__doc__)
success: bool = Field(
...,
description="A boolean indicator that the operation succeeded or failed. Allows programmatic assessment of "
"all results regardless of if they failed or succeeded by checking `result.success`.",
)

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 successful result"""

success: Literal[True] = Field(True, description="Always `True` for a successful result")
44 changes: 10 additions & 34 deletions qcelemental/models/molecule.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@

import hashlib
import json
import pdb
import warnings
from functools import partial
from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Tuple, Union, cast

import numpy as np
from pydantic import ConstrainedFloat, ConstrainedInt, Field, constr, validator
from pydantic import ConstrainedFloat, ConstrainedInt, Field, validator
from typing_extensions import Literal

# molparse imports separated b/c https://github.com/python/mypy/issues/7203
from ..molparse.from_arrays import from_arrays
from ..molparse.from_schema import from_schema
from ..molparse.from_string import from_string
Expand All @@ -22,9 +22,11 @@
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

# molparse imports separated b/c https://github.com/python/mypy/issues/7203
from .basemodels import ProtoModel, Provenance, qcschema_draft
from .types import Array
from .qcschema_abc import AutoSetProvenance

if TYPE_CHECKING:
from pydantic.typing import ReprArgs
Expand Down Expand Up @@ -94,7 +96,7 @@ class Config(ProtoModel.Config):
serialize_skip_defaults = True


class Molecule(ProtoModel):
class Molecule(AutoSetProvenance):
r"""
The physical Cartesian representation of the molecular system.
Expand All @@ -112,17 +114,8 @@ class Molecule(ProtoModel):
* <varies>: irregular dimension not systematically reshapable
"""
schema_name: Literal["qcschema_molecule"] = "qcschema_molecule"

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_version: int = Field( # type: ignore
2,
description="The version number of :attr:`~qcelemental.models.Molecule.schema_name` to which this model conforms.",
)
validated: bool = Field( # type: ignore
False,
description="A boolean indicator (for speed purposes) that the input Molecule data has been previously checked "
Expand Down Expand Up @@ -277,22 +270,6 @@ class Molecule(ProtoModel):
None,
description="Maximal point group symmetry which :attr:`~qcelemental.models.Molecule.geometry` should be treated. Lowercase.",
)
# Extra
provenance: Provenance = Field(
default_factory=partial(provenance_stamp, __name__),
description="The provenance information about how this Molecule (and its attributes) were generated, "
"provided, and manipulated.",
)
id: Optional[Any] = Field( # type: ignore
None,
description="A unique identifier for this Molecule object. This field exists primarily for Databases "
"(e.g. Fractal's Server) to track and lookup this specific object and should virtually "
"never need to be manually set.",
)
extras: Dict[str, Any] = Field( # type: ignore
None,
description="Additional information to bundle with the molecule. Use for schema development and scratch space.",
)

class Config(ProtoModel.Config):
serialize_skip_defaults = True
Expand Down Expand Up @@ -336,8 +313,8 @@ def __init__(self, orient: bool = False, validate: Optional[bool] = None, **kwar
geometry_noise = kwargs.pop("geometry_noise", GEOMETRY_NOISE)

if validate:
kwargs["schema_name"] = kwargs.pop("schema_name", "qcschema_molecule")
kwargs["schema_version"] = kwargs.pop("schema_version", 2)
kwargs["schema_name"] = kwargs.pop("schema_name", "qcschema_molecule")
# original_keys = set(kwargs.keys()) # revive when ready to revisit sparsity

nonphysical = kwargs.pop("nonphysical", False)
Expand Down Expand Up @@ -910,7 +887,6 @@ def from_data(
for key in charge_spin_opts - kwarg_keys:
input_dict.pop(key, None)
input_dict.pop("validated", None)

return cls(orient=orient, validate=validate, **input_dict)

@classmethod
Expand Down
Loading

0 comments on commit 6e341fd

Please sign in to comment.