Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Csse layout 536 input_data #358

Draft
wants to merge 13 commits into
base: next2024
Choose a base branch
from
10 changes: 10 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ New Features

Enhancements
++++++++++++
- (536b) ``v1.AtomicResult.convert_v`` learned a ``external_input_data`` option to inject that field (if known) rather than using incomplete reconstruction from the v1 Result. may not be the final sol'n.
- (536b) ``v2.FailedOperation`` gained schema_name and schema_version=2.
- (536b) ``v2.AtomicResult`` no longer inherits from ``v2.AtomicInput``. It gained a ``input_data`` field for the corresponding ``AtomicInput`` and independent ``id`` and ``molecule`` fields (the latter being equivalvent to ``v1.AtomicResult.molecule`` with the frame of the results; ``v2.AtomicResult.input_data.molecule`` is new, preserving the input frame). Gained independent ``extras``
- (536b) Both v1/v2 ``AtomicResult.convert_v()`` learned to handle the new ``input_data`` layout.
- (:pr:`357`, :issue:`536`) ``v2.AtomicResult``, ``v2.OptimizationResult``, and ``v2.TorsionDriveResult`` have the ``success`` field enforced to ``True``. Previously it could be set T/F. Now validation errors if not T. Likewise ``v2.FailedOperation.success`` is enforced to ``False``.
- (:pr:`357`, :issue:`536`) ``v2.AtomicResult``, ``v2.OptimizationResult``, and ``v2.TorsionDriveResult`` have the ``error`` field removed. This isn't used now that ``success=True`` and failure should be routed to ``FailedOperation``.
- (:pr:`357`) ``v1.Molecule`` had its schema_version changed to a Literal[2] (remember Mol is one-ahead of general numbering scheme) so new instances will be 2 even if another value is passed in. Ditto ``v2.BasisSet.schema_version=2``. Ditto ``v1.BasisSet.schema_version=1`` Ditto ``v1.QCInputSpecification.schema_version=1`` and ``v1.OptimizationSpecification.schema_version=1``.
- (:pr:`357`) ``v2.AtomicResultProperties``, ``v2.QCInputSpecification``, ``v2.OptimizationSpecification`` lost its schema_version until we see if its really needed.
- (:pr:`357`) ``v2.OptimizationSpecification`` gained extras field
- (:pr:`357`) ``v1.FailedOperation.extras`` and ``v2.FailedOperation.extras`` default changed from None to {}
* Fix a lot of warnings originating in this project.
* `Molecule.extras` now defaults to `{}` rather than None in both v1 and v2. Input None converts to {} upon instantiation.
* ``v2.FailedOperation`` field `id` is becoming `Optional[str]` instead of plain `str` so that the default validates.
Expand Down
2 changes: 1 addition & 1 deletion qcelemental/models/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@
DeprecationWarning,
)

# Array = qcelemental.models.v1.Array
Array = qcelemental.models.v1.Array
# ArrayMeta = qcelemental.models.v1.ArrayMeta
# TypedArray = qcelemental.models.v1.TypedArray
6 changes: 4 additions & 2 deletions qcelemental/models/v1/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from . import types
from . import types # ever used?
from .align import AlignmentMill
from .basemodels import AutodocBaseSettings # remove when QCFractal merges `next`
from .basemodels import ProtoModel
Expand All @@ -8,6 +8,7 @@
from .procedures import Optimization # scheduled for removal
from .procedures import (
OptimizationInput,
OptimizationProtocols,
OptimizationResult,
OptimizationSpecification,
QCInputSpecification,
Expand All @@ -18,7 +19,8 @@
from .results import Result # scheduled for removal
from .results import ResultInput # scheduled for removal
from .results import ResultProperties # scheduled for removal
from .results import AtomicInput, AtomicResult, AtomicResultProperties, AtomicResultProtocols
from .results import AtomicInput, AtomicResult, AtomicResultProperties, AtomicResultProtocols, WavefunctionProperties
from .types import Array


def qcschema_models():
Expand Down
12 changes: 11 additions & 1 deletion qcelemental/models/v1/basis.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
from enum import Enum
from typing import Dict, List, Optional

try:
from typing import Literal
except ImportError:
# remove when minimum py38
from typing_extensions import Literal

from pydantic.v1 import ConstrainedInt, Field, constr, validator

from ...exceptions import ValidationError
Expand Down Expand Up @@ -155,7 +161,7 @@ class BasisSet(ProtoModel):
"qcschema_basis",
description=(f"The QCSchema specification to which this model conforms. Explicitly fixed as qcschema_basis."),
)
schema_version: int = Field( # type: ignore
schema_version: Literal[1] = Field( # type: ignore
1,
description="The version number of :attr:`~qcelemental.models.BasisSet.schema_name` to which this model conforms.",
)
Expand All @@ -175,6 +181,10 @@ class Config(ProtoModel.Config):
def schema_extra(schema, model):
schema["$schema"] = qcschema_draft

@validator("schema_version", pre=True)
def _version_stamp(cls, v):
return 1

@validator("atom_map")
def _check_atom_map(cls, v, values):
sv = set(v)
Expand Down
3 changes: 2 additions & 1 deletion qcelemental/models/v1/common_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ class FailedOperation(ProtoModel):
":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.",
)
Expand All @@ -139,6 +139,7 @@ def convert_v(

dself = self.dict()
if version == 2:
# TODO if FailedOp gets a schema_version, add a validator
self_vN = qcel.models.v2.FailedOperation(**dself)

return self_vN
Expand Down
14 changes: 13 additions & 1 deletion qcelemental/models/v1/molecule.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@
from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Tuple, Union, cast

try:
from typing import Literal
except ImportError:
# remove when minimum py38
from typing_extensions import Literal

import numpy as np
from pydantic.v1 import ConstrainedFloat, ConstrainedInt, Field, constr, validator

Expand Down Expand Up @@ -119,7 +125,7 @@ class Molecule(ProtoModel):
f"The QCSchema specification to which this model conforms. Explicitly fixed as {qcschema_molecule_default}."
),
)
schema_version: int = Field( # type: ignore
schema_version: Literal[2] = Field( # type: ignore
2,
description="The version number of :attr:`~qcelemental.models.Molecule.schema_name` to which this model conforms.",
)
Expand Down Expand Up @@ -370,6 +376,12 @@ def __init__(self, orient: bool = False, validate: Optional[bool] = None, **kwar
elif validate or geometry_prep:
values["geometry"] = float_prep(values["geometry"], geometry_noise)

@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 2

@validator("geometry")
def _must_be_3n(cls, v, values, **kwargs):
n = len(values["symbols"])
Expand Down
34 changes: 32 additions & 2 deletions qcelemental/models/v1/procedures.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,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 # TODO
schema_version: Literal[1] = 1

driver: DriverEnum = Field(DriverEnum.gradient, description=str(DriverEnum.__doc__))
model: Model = Field(..., description=str(Model.__doc__))
Expand All @@ -71,6 +71,10 @@ class QCInputSpecification(ProtoModel):
description="Additional information to bundle with the computation. Use for schema development and scratch space.",
)

@validator("schema_version", pre=True)
def _version_stamp(cls, v):
return 1


class OptimizationInput(ProtoModel):
id: Optional[str] = None
Expand Down Expand Up @@ -110,6 +114,7 @@ def convert_v(

dself = self.dict()
if version == 2:
dself["input_specification"].pop("schema_version", None)
self_vN = qcel.models.v2.OptimizationInput(**dself)

return self_vN
Expand Down Expand Up @@ -171,8 +176,15 @@ def convert_v(
if check_convertible_version(version, error="OptimizationResult") == "self":
return self

trajectory_class = self.trajectory[0].__class__
dself = self.dict()
if version == 2:
# remove harmless empty error field that v2 won't accept. if populated, pydantic will catch it.
dself.pop("error", None)

dself["trajectory"] = [trajectory_class(**atres).convert_v(version) for atres in dself["trajectory"]]
dself["input_specification"].pop("schema_version", None)

self_vN = qcel.models.v2.OptimizationResult(**dself)

return self_vN
Expand All @@ -189,12 +201,16 @@ class OptimizationSpecification(ProtoModel):
"""

schema_name: constr(strip_whitespace=True, regex="qcschema_optimization_specification") = "qcschema_optimization_specification" # type: ignore
schema_version: int = 1 # TODO
schema_version: Literal[1] = 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("schema_version", pre=True)
def _version_stamp(cls, v):
return 1

@validator("procedure")
def _check_procedure(cls, v):
return v.lower()
Expand Down Expand Up @@ -282,6 +298,9 @@ def convert_v(

dself = self.dict()
if version == 2:
dself["input_specification"].pop("schema_version", None)
dself["optimization_spec"].pop("schema_version", None)

self_vN = qcel.models.v2.TorsionDriveInput(**dself)

return self_vN
Expand Down Expand Up @@ -332,8 +351,19 @@ def convert_v(
if check_convertible_version(version, error="TorsionDriveResult") == "self":
return self

opthist_class = next(iter(self.optimization_history.values()))[0].__class__
dself = self.dict()
if version == 2:
# remove harmless empty error field that v2 won't accept. if populated, pydantic will catch it.
dself.pop("error", None)

dself["input_specification"].pop("schema_version", None)
dself["optimization_spec"].pop("schema_version", None)
dself["optimization_history"] = {
k: [opthist_class(**res).convert_v(version) for res in lst]
for k, lst in dself["optimization_history"].items()
}

self_vN = qcel.models.v2.TorsionDriveResult(**dself)

return self_vN
Expand Down
47 changes: 45 additions & 2 deletions qcelemental/models/v1/results.py
Original file line number Diff line number Diff line change
Expand Up @@ -797,16 +797,59 @@ def _native_file_protocol(cls, value, values):
return ret

def convert_v(
self, version: int
self,
version: int,
*,
external_input_data: Optional[Any] = None,
) -> Union["qcelemental.models.v1.AtomicResult", "qcelemental.models.v2.AtomicResult"]:
"""Convert to instance of particular QCSchema version."""
"""Convert to instance of particular QCSchema version.

Parameters
----------
version
The version to convert to.
external_input_data
Since self contains data merged from input, this allows passing in the original input, particularly for `molecule` and `extras` fields.
Can be model or dictionary and should be *already* converted to the desired version.
Replaces ``input_data`` field entirely (not merges with extracts from self) and w/o consistency checking.

Returns
-------
AtomicResult
Returns self (not a copy) if ``version`` already satisfied.
Returns a new AtomicResult of ``version`` otherwise.

"""
import qcelemental as qcel

if check_convertible_version(version, error="AtomicResult") == "self":
return self

dself = self.dict()
if version == 2:
# remove harmless empty error field that v2 won't accept. if populated, pydantic will catch it.
dself.pop("error", None)

input_data = {
k: dself.pop(k) for k in list(dself.keys()) if k in ["driver", "keywords", "model", "protocols"]
}
input_data["molecule"] = dself["molecule"] # duplicate since input mol has been overwritten
# any input provenance has been overwritten
input_data["extras"] = {
k: dself["extras"].pop(k) for k in list(dself["extras"].keys()) if k in []
} # sep any merged extras
if external_input_data:
# Note: overwriting with external, not updating. reconsider?
dself["input_data"] = external_input_data
in_extras = (
external_input_data.get("extras", {})
if isinstance(external_input_data, dict)
else external_input_data.extras
)
dself["extras"] = {k: v for k, v in dself["extras"].items() if (k, v) not in in_extras.items()}
else:
dself["input_data"] = input_data

self_vN = qcel.models.v2.AtomicResult(**dself)

return self_vN
Expand Down
13 changes: 11 additions & 2 deletions qcelemental/models/v2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,17 @@
from .basis import BasisSet
from .common_models import ComputeError, DriverEnum, FailedOperation, Model, Provenance
from .molecule import Molecule
from .procedures import OptimizationInput, OptimizationResult, TorsionDriveInput, TorsionDriveResult
from .results import AtomicInput, AtomicResult, AtomicResultProperties, AtomicResultProtocols
from .procedures import (
OptimizationInput,
OptimizationProtocols,
OptimizationResult,
OptimizationSpecification,
QCInputSpecification,
TDKeywords,
TorsionDriveInput,
TorsionDriveResult,
)
from .results import AtomicInput, AtomicResult, AtomicResultProperties, AtomicResultProtocols, WavefunctionProperties


def qcschema_models():
Expand Down
12 changes: 11 additions & 1 deletion qcelemental/models/v2/basis.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
from enum import Enum
from typing import Dict, List, Optional

try:
from typing import Literal
except ImportError:
# remove when minimum py38
from typing_extensions import Literal

from pydantic import Field, constr, field_validator
from typing_extensions import Annotated

Expand Down Expand Up @@ -171,7 +177,7 @@ class BasisSet(ProtoModel):
"qcschema_basis",
description=f"The QCSchema specification to which this model conforms. Explicitly fixed as qcschema_basis.",
)
schema_version: int = Field( # type: ignore
schema_version: Literal[2] = Field( # type: ignore
2,
description="The version number of :attr:`~qcelemental.models.BasisSet.schema_name` "
"to which this model conforms.",
Expand Down Expand Up @@ -245,3 +251,7 @@ def _calculate_nbf(cls, atom_map, center_data) -> int:
ret += center_count[center]

return ret

@field_validator("schema_version", mode="before")
def _version_stamp(cls, v):
return 2
Loading
Loading