diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index d6f1f0ff..40d1cb40 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -11,10 +11,13 @@ jobs: cfg: - conda-env: minimal python-version: 3.6 + label: mindep - conda-env: base python-version: 3.6 + label: minpy - conda-env: base python-version: 3.8 + label: full env: PYVER: ${{ matrix.cfg.python-version }} CONDA_ENV: ${{ matrix.cfg.conda-env }} @@ -54,5 +57,12 @@ jobs: eval "$(conda shell.bash hook)" && conda activate test pytest -rws -v --cov=qcelemental --color=yes --cov-report=xml qcelemental/ + - name: PyTest Validate + shell: bash + if: matrix.cfg.label == 'full' + run: | + eval "$(conda shell.bash hook)" && conda activate test + pytest -rws -v --color=yes --validate qcelemental/ + - name: CodeCov uses: codecov/codecov-action@v1 diff --git a/.github/workflows/QCSchema.yml b/.github/workflows/QCSchema.yml new file mode 100644 index 00000000..44878bd3 --- /dev/null +++ b/.github/workflows/QCSchema.yml @@ -0,0 +1,91 @@ +name: QCSchema + +on: [pull_request] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + conda-env: [base] + python-version: [3.7] + env: + PYVER: ${{ matrix.python-version }} + CONDA_ENV: ${{ matrix.conda-env }} + + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + path: qcel + + - name: Checkout schema repo + uses: actions/checkout@v2 + with: + repository: MolSSI/QCSchema + path: qcsk + ref: qcsk_export_2 + #ref: master +# persist-credentials: false + fetch-depth: 0 + token: ${{ secrets.qcschema_from_qcelemental }} + + - name: Python Setup + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + + - name: Create Environment + shell: bash + working-directory: ./qcel + run: | + eval "$(conda shell.bash hook)" && conda activate + python devtools/scripts/create_conda_env.py -n=test -p=$PYVER devtools/conda-envs/$CONDA_ENV.yaml + + - name: Install + shell: bash + working-directory: ./qcel + run: | + eval "$(conda shell.bash hook)" && conda activate test + python -m pip install . --no-deps + + - name: Environment Information + shell: bash + run: | + eval "$(conda shell.bash hook)" && conda activate test + conda list --show-channel-urls + + - name: QCSchema from QCElemental + shell: bash + working-directory: ./qcel + run: | + eval "$(conda shell.bash hook)" && conda activate test + make qcschema + ls -l qcschema + cp -p qcschema/* ../qcsk/qcschema/data/vdev/ + mv ../qcsk/qcschema/data/vdev/QCSchema.schema ../qcsk/qcschema/dev/ + + - name: Compare Schemas (generated vs. community) + shell: bash + working-directory: ./qcsk + run: | + git diff --color-words + pull_number=$(jq --raw-output .pull_request.number "$GITHUB_EVENT_PATH") + branch=qcel-${pull_number} + git checkout -b ${branch} + git remote -v + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + git add -A + git commit -m "auto-generated from QCElemental" + echo "::set-env name=prbranch::${branch}" + + - name: Propose changes + uses: ad-m/github-push-action@master + with: + directory: ./qcsk + repository: MolSSI/QCSchema + branch: ${{ env.prbranch }} +# github_token: ${{ secrets.GITHUB_TOKEN }} + github_token: ${{ secrets.qcschema_from_qcelemental }} + force: true diff --git a/.gitignore b/.gitignore index 666c7467..45f83e57 100644 --- a/.gitignore +++ b/.gitignore @@ -119,3 +119,7 @@ runinfo/* # VSCode .vscode/ raw_data/**/*_blob.py + +# autogen +qcschema/*.schema +qcelemental/tests/qcschema_instances/*/*.json diff --git a/Makefile b/Makefile index 2bcb96c1..386d0c20 100644 --- a/Makefile +++ b/Makefile @@ -43,6 +43,12 @@ data: cpu_data cpu_data: (cd raw_data/cpu_data; python build_cpu_data.py; mv cpu_data_blob.py ../../qcelemental/info/data/) +.PHONY: qcschema +qcschema: + mkdir -p qcschema + python -c "exec(\"import pathlib, qcelemental\nfor md in qcelemental.models.qcschema_models():\n\tmfile = (pathlib.Path('qcschema') / md.__name__).with_suffix('.schema')\n\twith open(mfile, 'w') as fp:\n\t\tfp.write(md.schema_json(indent=None))\")" + python -c "exec(\"import json, pathlib, pydantic, qcelemental\nwith open((pathlib.Path('qcschema') / 'QCSchema').with_suffix('.schema'), 'w') as fp:\n\tjson.dump(pydantic.schema.schema(qcelemental.models.qcschema_models(), title='QCSchema'), fp, indent=4)\")" + .PHONY: clean clean: rm -rf `find . -name __pycache__` diff --git a/devtools/conda-envs/base.yaml b/devtools/conda-envs/base.yaml index 0350c9f3..ec43b74a 100644 --- a/devtools/conda-envs/base.yaml +++ b/devtools/conda-envs/base.yaml @@ -20,3 +20,4 @@ dependencies: - pytest-cov - codecov - scipy # tests an aspect of a helper fn not used by qcel functionality + - jsonschema diff --git a/devtools/conda-envs/minimal.yaml b/devtools/conda-envs/minimal.yaml index bf609d50..46591ed4 100644 --- a/devtools/conda-envs/minimal.yaml +++ b/devtools/conda-envs/minimal.yaml @@ -8,9 +8,10 @@ dependencies: - nomkl - python - pint=0.10.0 # technically, qcel has no lower bound for pint version for py36,37 but needs 0.10 for 38 - - pydantic=1.2.0 # technically, qcel works with 1.0.0 but c-f doesn't have py38 builds for it + - pydantic=1.5.0 # Testing - pytest=4.6.4 # technically, qcel works with 4.0.0 but c-f doesn't have py38 builds for it - pytest-cov - codecov + - jsonschema diff --git a/qcelemental/conftest.py b/qcelemental/conftest.py new file mode 100644 index 00000000..67fb4a5b --- /dev/null +++ b/qcelemental/conftest.py @@ -0,0 +1,29 @@ +from pathlib import Path + +import pytest + + +def pytest_addoption(parser): + parser.addoption( + "--validate", action="store_true", help="validate JSON from previous test run against exported schema" + ) + + +@pytest.fixture(scope="session", autouse=True) +def set_up_overall(request): + # in all pytest runs except --validate (which uses the files), clear away the JSON examples and generate fresh + if not request.config.getoption("--validate", default=False): + _data_path = Path(__file__).parent.resolve() / "tests" / "qcschema_instances" + for fl in _data_path.rglob("*.json"): + fl.unlink() + + +def pytest_runtest_setup(item): + # there's a bug where can only set options if specify path in call, so needs to be ``pytest qcelemental/ --validate`` + + # skip the validate-generated-instances-against-exported-schema tests on most ``pytest`` runs. + # run only the validate-generated-instances-against-exported-schema tests on ``pytest --validate`` runs. + if not item.config.getoption("--validate", default=False) and item.name.startswith("test_qcschema"): + pytest.skip("can't run with --validate option") + elif item.config.getoption("--validate", default=False) and not item.name.startswith("test_qcschema"): + pytest.skip("need --validate option to run") diff --git a/qcelemental/info/cpu_info.py b/qcelemental/info/cpu_info.py index d2719db7..ce1c4a00 100644 --- a/qcelemental/info/cpu_info.py +++ b/qcelemental/info/cpu_info.py @@ -14,8 +14,7 @@ class VendorEnum(str, Enum): - """Allowed processor vendors, used for validation. - """ + """Allowed processor vendors, used for validation.""" amd = "amd" intel = "intel" @@ -24,8 +23,7 @@ class VendorEnum(str, Enum): class InstructionSetEnum(int, Enum): - """Allowed instruction sets for CPUs in an ordinal enum. - """ + """Allowed instruction sets for CPUs in an ordinal enum.""" none = 0 sse = 1 diff --git a/qcelemental/models/__init__.py b/qcelemental/models/__init__.py index f2c6102c..4a69f39a 100644 --- a/qcelemental/models/__init__.py +++ b/qcelemental/models/__init__.py @@ -12,5 +12,18 @@ from .basis import BasisSet from .common_models import ComputeError, DriverEnum, FailedOperation, Provenance from .molecule import Molecule -from .procedures import Optimization, OptimizationInput, OptimizationResult -from .results import AtomicInput, AtomicResult, AtomicResultProperties, Result, ResultInput, ResultProperties +from .procedures import OptimizationInput, OptimizationResult +from .procedures import Optimization # scheduled for removal +from .results import AtomicInput, AtomicResult, AtomicResultProperties +from .results import Result, ResultInput, ResultProperties # scheduled for removal + + +def qcschema_models(): + return [ + AtomicInput, + AtomicResult, + AtomicResultProperties, + BasisSet, + Molecule, + Provenance, + ] diff --git a/qcelemental/models/align.py b/qcelemental/models/align.py index 6e21faeb..e41d1a2f 100644 --- a/qcelemental/models/align.py +++ b/qcelemental/models/align.py @@ -21,15 +21,9 @@ class AlignmentMill(ProtoModel): """ - shift: Optional[Array[float]] = Field( # type: ignore - None, description="Translation array (3,) for coordinates." - ) - rotation: Optional[Array[float]] = Field( # type: ignore - None, description="Rotation array (3, 3) for coordinates." - ) - atommap: Optional[Array[int]] = Field( # type: ignore - None, description="Atom exchange map (nat,) for coordinates." - ) + shift: Optional[Array[float]] = Field(None, description="Translation array (3,) for coordinates.") # type: ignore + rotation: Optional[Array[float]] = Field(None, description="Rotation array (3, 3) for coordinates.") # type: ignore + atommap: Optional[Array[int]] = Field(None, description="Atom exchange map (nat,) for coordinates.") # type: ignore mirror: bool = Field(False, description="Do mirror invert coordinates?") class Config: diff --git a/qcelemental/models/basemodels.py b/qcelemental/models/basemodels.py index eb99a845..1cd4328d 100644 --- a/qcelemental/models/basemodels.py +++ b/qcelemental/models/basemodels.py @@ -125,6 +125,8 @@ def serialize( include: Optional[Set[str]] = None, exclude: Optional[Set[str]] = None, exclude_unset: Optional[bool] = None, + exclude_defaults: Optional[bool] = None, + exclude_none: Optional[bool] = None, ) -> Union[bytes, str]: """Generates a serialized representation of the model @@ -138,6 +140,10 @@ def serialize( Fields to be excluded in the serialization. exclude_unset : Optional[bool], optional If True, skips fields that have default values provided. + exclude_defaults: Optional[bool], optional + If True, skips fields that have set or defaulted values equal to the default. + exclude_none: Optional[bool], optional + If True, skips fields that have value ``None``. Returns ------- @@ -152,6 +158,10 @@ def serialize( kwargs["exclude"] = exclude if exclude_unset: kwargs["exclude_unset"] = exclude_unset + if exclude_defaults: + kwargs["exclude_defaults"] = exclude_defaults + if exclude_none: + kwargs["exclude_none"] = exclude_none data = self.dict(**kwargs) @@ -182,3 +192,6 @@ def compare(self, other: Union["ProtoModel", BaseModel], **kwargs) -> bool: class AutodocBaseSettings(BaseSettings): def __init_subclass__(cls) -> None: cls.__doc__ = AutoPydanticDocGenerator(cls, always_apply=True) + + +qcschema_draft = "http://json-schema.org/draft-04/schema#" diff --git a/qcelemental/models/basis.py b/qcelemental/models/basis.py index 952bd940..528085ea 100644 --- a/qcelemental/models/basis.py +++ b/qcelemental/models/basis.py @@ -1,10 +1,14 @@ from enum import Enum from typing import Dict, List, Optional -from pydantic import Field, constr, validator +from pydantic import ConstrainedInt, Field, constr, validator from ..exceptions import ValidationError -from .basemodels import ProtoModel +from .basemodels import ProtoModel, qcschema_draft + + +class NonnegativeInt(ConstrainedInt): + ge = 0 class HarmonicType(str, Enum): @@ -21,14 +25,26 @@ class ElectronShell(ProtoModel): Information for a single electronic shell """ - angular_momentum: List[int] = Field(..., description="Angular momentum for this shell.") + angular_momentum: List[NonnegativeInt] = Field( + ..., description="Angular momentum for the shell as an array of integers.", min_items=1 + ) + # function_type: FunctionType = Field(..., description=str(FunctionType.__doc__)) + # region: Region = Field(None, description=str(Region.__doc__)) harmonic_type: HarmonicType = Field(..., description=str(HarmonicType.__doc__)) - exponents: List[float] = Field(..., description="Exponents for this contracted shell.") + exponents: List[float] = Field(..., description="Exponents for the contracted shell.", min_items=1) coefficients: List[List[float]] = Field( ..., - description="General contraction coefficients for this shell, individual list components will be the individual segment contraction coefficients.", + description="General contraction coefficients for the shell, individual list components will be the individual segment contraction coefficients.", + min_items=1, ) + class Config(ProtoModel.Config): + def schema_extra(schema, model): + # edit to allow string storage of basis sets as BSE uses. alternately, could `Union[float, str]` above but that loses some validation + schema["properties"]["exponents"]["items"] = {"anyOf": [{"type": "number"}, {"type": "string"}]} + schema["properties"]["coefficients"]["items"]["items"] = {"anyOf": [{"type": "number"}, {"type": "string"}]} + schema["properties"]["angular_momentum"].update({"uniqueItems": True}) + @validator("coefficients") def _check_coefficient_length(cls, v, values): len_exp = len(values["exponents"]) @@ -89,14 +105,24 @@ class ECPPotential(ProtoModel): """ ecp_type: ECPType = Field(..., description=str(ECPType.__doc__)) - angular_momentum: List[int] = Field(..., description="Angular momentum for the ECPs.") - r_exponents: List[int] = Field(..., description="Exponents of the 'r' term.") - gaussian_exponents: List[float] = Field(..., description="Exponents of the 'gaussian' term.") + angular_momentum: List[NonnegativeInt] = Field( + ..., description="Angular momentum for the potential as an array of integers.", min_items=1 + ) + r_exponents: List[int] = Field(..., description="Exponents of the 'r' term.", min_items=1) + gaussian_exponents: List[float] = Field(..., description="Exponents of the 'gaussian' term.", min_items=1) coefficients: List[List[float]] = Field( ..., - description="General contraction coefficients for this shell, individual list components will be the individual segment contraction coefficients.", + description="General contraction coefficients for the potential, individual list components will be the individual segment contraction coefficients.", + min_items=1, ) + class Config(ProtoModel.Config): + def schema_extra(schema, model): + # edit to allow string storage of basis sets as BSE uses. alternately, could `Union[float, str]` above but that loses some validation + schema["properties"]["gaussian_exponents"]["items"] = {"anyOf": [{"type": "number"}, {"type": "string"}]} + schema["properties"]["coefficients"]["items"]["items"] = {"anyOf": [{"type": "number"}, {"type": "string"}]} + schema["properties"]["angular_momentum"].update({"uniqueItems": True}) + @validator("gaussian_exponents") def _check_gaussian_exponents_length(cls, v, values): len_exp = len(values["r_exponents"]) @@ -120,9 +146,16 @@ class BasisCenter(ProtoModel): Data for a single atom/center in a basis set. """ - electron_shells: List[ElectronShell] = Field(..., description="Electronic shells for this center.") - ecp_electrons: int = Field(0, description="Number of electrons replace by ECP potentials.") - ecp_potentials: Optional[List[ECPPotential]] = Field(None, description="ECPs for this center.") + electron_shells: List[ElectronShell] = Field(..., description="Electronic shells for this center.", min_items=1) + ecp_electrons: int = Field(0, description="Number of electrons replaced by ECP, MCP, or other field potentials.") + ecp_potentials: Optional[List[ECPPotential]] = Field( + None, description="ECPs, MCPs, or other field potentials for this center.", min_items=1 + ) + + class Config(ProtoModel.Config): + def schema_extra(schema, model): + schema["properties"]["electron_shells"].update({"uniqueItems": True}) + schema["properties"]["ecp_potentials"].update({"uniqueItems": True}) class BasisSet(ProtoModel): @@ -130,18 +163,29 @@ class BasisSet(ProtoModel): A quantum chemistry basis description. """ - schema_name: constr(strip_whitespace=True, regex="qcschema_basis") = "qcschema_basis" - schema_version: int = 1 + schema_name: constr(strip_whitespace=True, regex="^(qcschema_basis)$") = Field( # type: ignore + "qcschema_basis", + description=(f"The QCSchema specification to which this model conforms. Explicitly fixed as qcschema_basis."), + ) + schema_version: int = Field( # type: ignore + 1, description="The version number of ``schema_name`` to which this model conforms." + ) name: str = Field(..., description="A standard basis name if available (e.g., 'cc-pVDZ').") description: Optional[str] = Field(None, description="A brief description of the basis set.") - center_data: Dict[str, BasisCenter] = Field(..., description="A mapping of all types of centers available.") + center_data: Dict[str, BasisCenter] = Field( + ..., description="Shared basis data for all atoms/centers in the parent molecule" + ) atom_map: List[str] = Field( - ..., description="Mapping of all centers in the parent molecule to centers in `center_data`." + ..., description="Mapping of all atoms/centers in the parent molecule to centers in `center_data`." ) nbf: Optional[int] = Field(None, description="The number of basis functions.") + class Config(ProtoModel.Config): + def schema_extra(schema, model): + schema["$schema"] = qcschema_draft + @validator("atom_map") def _check_atom_map(cls, v, values): sv = set(v) diff --git a/qcelemental/models/common_models.py b/qcelemental/models/common_models.py index 8c29df68..59e5a409 100644 --- a/qcelemental/models/common_models.py +++ b/qcelemental/models/common_models.py @@ -1,10 +1,11 @@ from enum import Enum -from typing import TYPE_CHECKING, Any, Dict, Optional +from typing import TYPE_CHECKING, Any, Dict, Optional, Union import numpy as np from pydantic import Field -from .basemodels import ProtoModel +from .basemodels import ProtoModel, qcschema_draft +from .basis import BasisSet if TYPE_CHECKING: from pydantic.typing import ReprArgs @@ -19,39 +20,50 @@ class Provenance(ProtoModel): Provenance information. """ - creator: str = Field(..., description="The creator of the object.") - version: Optional[str] = Field(None, description="The version of the creator.") - routine: Optional[str] = Field(None, description="The routine of the creator.") + # compared to qcsk v2: + # this has defaults for version & routine, and those aren't required. + # this doesn't explicitly set `additionalProperties = True` + + 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 = "allow" + extra: str = "allow" + + def schema_extra(schema, model): + schema["$schema"] = qcschema_draft class Model(ProtoModel): """ - The quantum chemistry model specification for a given operation to compute against + The computational molecular sciences model to run. """ method: str = Field( # type: ignore - ..., description="The quantum chemistry method to evaluate (e.g., B3LYP, PBE, ...)." + ..., + description="The quantum chemistry method to evaluate (e.g., B3LYP, PBE, ...). " + "For MM, name of the force field.", ) - basis: Optional[str] = Field( # type: ignore + basis: Optional[Union[str, BasisSet]] = Field( # type: ignore None, description="The quantum chemistry basis set to evaluate (e.g., 6-31g, cc-pVDZ, ...). Can be ``None`` for " - "methods without basis sets.", + "methods without basis sets. For molecular mechanics, name of the atom-typer.", ) # basis_spec: BasisSpec = None # This should be exclusive with basis, but for now will be omitted class Config(ProtoModel.Config): canonical_repr = True - extra = "allow" + extra: str = "allow" class DriverEnum(str, Enum): - """Allowed quantum chemistry driver values. - """ + """Allowed computation driver values.""" energy = "energy" gradient = "gradient" @@ -71,7 +83,7 @@ class ComputeError(ProtoModel): error_type: str = Field( # type: ignore ..., # Error enumeration not yet strict - description="The type of error which was thrown. Restrict this field short classifiers e.g. 'input_error'.", + description="The type of error which was thrown. Restrict this field to short classifiers e.g. 'input_error'. Suggested classifiers: https://github.com/MolSSI/QCEngine/blob/master/qcengine/exceptions.py", ) error_message: str = Field( # type: ignore ..., @@ -79,7 +91,7 @@ class ComputeError(ProtoModel): "information as well.", ) extras: Optional[Dict[str, Any]] = Field( # type: ignore - None, description="Additional data to ship with the ComputeError object." + None, description="Additional data to ship with the error object." ) class Config: diff --git a/qcelemental/models/molecule.py b/qcelemental/models/molecule.py index a7ba80ba..5bd38ae1 100644 --- a/qcelemental/models/molecule.py +++ b/qcelemental/models/molecule.py @@ -5,25 +5,30 @@ import hashlib import json import warnings +from functools import partial from pathlib import Path -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union +from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Tuple, Union, cast import numpy as np -from pydantic import Field, constr, validator - -from ..molparse import from_arrays, from_schema, from_string, to_schema, to_string +from pydantic import ConstrainedFloat, ConstrainedInt, Field, constr, validator + +# 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 +from ..molparse.to_schema import to_schema +from ..molparse.to_string import to_string from ..periodic_table import periodictable 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 +from .basemodels import ProtoModel, qcschema_draft from .common_models import Provenance, qcschema_molecule_default from .types import Array if TYPE_CHECKING: from pydantic.typing import ReprArgs - # Rounding quantities for hashing GEOMETRY_NOISE = 8 MASS_NOISE = 6 @@ -59,6 +64,15 @@ def float_prep(array, around): return array +class NonnegativeInt(ConstrainedInt): + ge = 0 + + +class BondOrderFloat(ConstrainedFloat): + ge = 0 + le = 5 + + class Identifiers(ProtoModel): """Canonical chemical identifiers""" @@ -82,20 +96,22 @@ class Config(ProtoModel.Config): class Molecule(ProtoModel): """ + The physical Cartesian representation of the molecular system. + A QCSchema representation of a Molecule. This model contains data for symbols, geometry, connectivity, charges, fragmentation, etc while also supporting a wide array of I/O and manipulation capabilities. Molecule objects geometry, masses, and charges are truncated to 8, 6, and 4 decimal places respectively to assist with duplicate detection. """ - schema_name: constr(strip_whitespace=True, regex=qcschema_molecule_default) = Field( # type: ignore + schema_name: constr(strip_whitespace=True, regex="^(qcschema_molecule)$") = Field( # type: ignore qcschema_molecule_default, description=( - f"The QCSchema specification this model conforms to. Explicitly fixed as " f"{qcschema_molecule_default}." + 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 ``schema_name`` that this Molecule model conforms to." + 2, description="The version number of ``schema_name`` to which this model conforms." ) validated: bool = Field( # type: ignore False, @@ -109,10 +125,11 @@ class Molecule(ProtoModel): # Required data symbols: Array[str] = Field( # type: ignore ..., - description="An ordered (nat,) array-like object of atomic elemental symbols of shape (nat,). The index of " - "this attribute sets atomic order for all other per-atom setting like ``real`` and the first " - "dimension of ``geometry``. Ghost/Virtual atoms must have an entry in this array-like and are " - "indicated by the matching the 0-indexed indices in ``real`` field.", + description="An ordered (nat,) array-like object of atomic elemental symbols in title case. The index of " + "this attribute sets atomic order for all other per-atom fields like ``real`` and the first " + "dimension of ``geometry``. Ghost/Virtual atoms must have an entry here in ``symbols``; ghostedness is " + "indicated through the ``real`` field.", + shape=["nat"], ) geometry: Array[float] = Field( # type: ignore ..., @@ -122,9 +139,11 @@ class Molecule(ProtoModel): "(e.g., gradient). Index of the first dimension matches the 0-indexed indices of all other " "per-atom settings like ``symbols`` and ``real``." "\n" - "Can also accept array-likes which can be mapped to (nat,3) such as a 1-D list of length 3*nat, " + "Serialized storage is always flat, (3*nat,), but QCSchema implementations will want to reshape it. " + "QCElemental can also accept array-likes which can be mapped to (nat,3) such as a 1-D list of length 3*nat, " "or the serialized version of the array in (3*nat,) shape; all forms will be reshaped to " "(nat,3) for this attribute.", + shape=["nat", 3], ) # Molecule data @@ -138,19 +157,20 @@ class Molecule(ProtoModel): ) comment: Optional[str] = Field( # type: ignore None, - description="Additional comments for this Molecule. Intended for pure human/user consumption " "and clarity.", + description="Additional comments for this Molecule. Intended for pure human/user consumption and clarity.", ) - molecular_charge: float = Field(0.0, description="The net electrostatic charge of this Molecule.") # type: ignore - molecular_multiplicity: int = Field(1, description="The total multiplicity of this Molecule.") # type: ignore + molecular_charge: float = Field(0.0, description="The net electrostatic charge of the Molecule.") # type: ignore + molecular_multiplicity: int = Field(1, description="The total multiplicity of the Molecule.") # type: ignore # Atom data masses_: Optional[Array[float]] = Field( # type: ignore None, description="An ordered 1-D array-like object of atomic masses [u] of shape (nat,). Index order " - "matches the 0-indexed indices of all other per-atom settings like ``symbols`` and ``real``. If " + "matches the 0-indexed indices of all other per-atom fields like ``symbols`` and ``real``. If " "this is not provided, the mass of each atom is inferred from their most common isotope. If this " "is provided, it must be the same length as ``symbols`` but can accept ``None`` entries for " "standard masses to infer from the same index in the ``symbols`` field.", + shape=["nat"], ) real_: Optional[Array[bool]] = Field( # type: ignore None, @@ -158,75 +178,91 @@ class Molecule(ProtoModel): "ghost/virtual (``False``). Index " "matches the 0-indexed indices of all other per-atom settings like ``symbols`` and the first " "dimension of ``geometry``. If this is not provided, all atoms are assumed to be real (``True``)." - "If this is provided, the reality or ghostality of every atom must be specified.", + "If this is provided, the reality or ghostedness of every atom must be specified.", + shape=["nat"], ) atom_labels_: Optional[Array[str]] = Field( # type: ignore None, description="Additional per-atom labels as a 1-D array-like of of strings of shape (nat,). Typical use is in " "model conversions, such as Elemental <-> Molpro and not typically something which should be user " "assigned. See the ``comments`` field for general human-consumable text to affix to the Molecule.", + shape=["nat"], ) atomic_numbers_: Optional[Array[np.int16]] = Field( # type: ignore None, description="An optional ordered 1-D array-like object of atomic numbers of shape (nat,). Index " "matches the 0-indexed indices of all other per-atom settings like ``symbols`` and ``real``. " - "Values are inferred from the ``symbols`` list if not explicitly set.", + "Values are inferred from the ``symbols`` list if not explicitly set. " + "Ghostedness should be indicated through ``real`` field, not zeros here.", + shape=["nat"], ) mass_numbers_: Optional[Array[np.int16]] = Field( # type: ignore None, description="An optional ordered 1-D array-like object of atomic *mass* numbers of shape (nat). Index " "matches the 0-indexed indices of all other per-atom settings like ``symbols`` and ``real``. " - "Values are inferred from the most common isotopes of the ``symbols`` list if not explicitly set.", + "Values are inferred from the most common isotopes of the ``symbols`` list if not explicitly set. " + "If single isotope not (yet) known for an atom, -1 is placeholder.", + shape=["nat"], ) # Fragment and connection data - connectivity_: Optional[List[Tuple[int, int, float]]] = Field( # type: ignore + connectivity_: Optional[List[Tuple[NonnegativeInt, NonnegativeInt, BondOrderFloat]]] = Field( # type: ignore None, - description="The connectivity information between each atom in the ``symbols`` array. Each entry in this " - "list is a Tuple of ``(atom_index_A, atom_index_B, bond_order)`` where the ``atom_index`` " - "matches the 0-indexed indices of all other per-atom settings like ``symbols`` and ``real``.", + description="A list of bonds within the molecule. Each entry is a tuple " + "of ``(atom_index_A, atom_index_B, bond_order)`` where the ``atom_index`` " + "matches the 0-indexed indices of all other per-atom settings like ``symbols`` and ``real``. " + "Bonds may be freely reordered and inverted.", + min_items=1, ) fragments_: Optional[List[Array[np.int32]]] = Field( # type: ignore None, - description="An indication of which sets of atoms are fragments within the Molecule. This is a list of shape " + description="List of indices grouping atoms (0-indexed) into molecular fragments within the Molecule. This is a list of shape " "(nfr) of 1-D array-like objects of arbitrary length. Each entry in the list indicates a new " "fragment. The index " "of the list matches the 0-indexed indices of ``fragment_charges`` and " "``fragment_multiplicities``. The 1-D array-like objects are sets of atom indices indicating the " "atoms which compose the fragment. The atom indices match the 0-indexed indices of all other " - "per-atom settings like ``symbols`` and ``real``.", + "per-atom settings like ``symbols`` and ``real``. " + "Fragment ordering is fixed; that is, a consumer who shuffles fragments must not reattach the input (pre-shuffling) molecule schema instance to any output (post-shuffling) per-fragment results (e.g., n-body energy arrays).", + shape=["nfr", ""], ) fragment_charges_: Optional[List[float]] = Field( # type: ignore None, description="The total charge of each fragment in the ``fragments`` list of shape (nfr,). The index of this " - "list matches the 0-index indices of ``fragment`` list. Will be filled in based on a set of rules " + "list matches the 0-index indices of ``fragments`` list. Will be filled in based on a set of rules " "if not provided (and ``fragments`` are specified).", + shape=["nfr"], ) fragment_multiplicities_: Optional[List[int]] = Field( # type: ignore None, description="The multiplicity of each fragment in the ``fragments`` list of shape (nfr,). The index of this " - "list matches the 0-index indices of ``fragment`` list. Will be filled in based on a set of " + "list matches the 0-index indices of ``fragments`` list. Will be filled in based on a set of " "rules if not provided (and ``fragments`` are specified).", + shape=["nfr"], ) # Orientation fix_com: bool = Field( # type: ignore False, - description="An indicator which prevents pre-processing the Molecule object to translate the Center-of-Mass " - "to (0,0,0) in euclidean coordinate space. Will result in a different ``geometry`` than the " - "one provided if False.", + description="Whether translation of geometry is allowed (fix F) or disallowed (fix T)." + "When False, QCElemental will pre-process the Molecule object to translate the center of mass " + "to (0,0,0) in Euclidean coordinate space, resulting in a different ``geometry`` than the " + "one provided. " + "guidance: A consumer who translates the geometry must not reattach the input (pre-translation) molecule schema instance to any output (post-translation) origin-sensitive results (e.g., an ordinary energy when EFP present).", ) fix_orientation: bool = Field( # type: ignore False, - description="An indicator which prevents pre-processes the Molecule object to orient via the inertia tensor." - "Will result in a different ``geometry`` than the one provided if False.", + description="Whether rotation of geometry is allowed (fix F) or disallowed (fix T). " + "When False, QCElemental will pre-process the Molecule object to orient via the intertial tensor, " + "resulting in a different ``geometry`` than the one provided. " + "guidance: A consumer who rotates the geometry must not reattach the input (pre-rotation) molecule schema instance to any output (post-rotation) frame-sensitive results (e.g., molecular vibrations).", ) fix_symmetry: Optional[str] = Field( # type: ignore None, description="Maximal point group symmetry which ``geometry`` should be treated. Lowercase." ) # Extra - provenance: Provenance = Field( # type: ignore - provenance_stamp(__name__), + 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.", ) @@ -259,6 +295,10 @@ class Config(ProtoModel.Config): "fragment_multiplicities_": "fragment_multiplicities", } + def schema_extra(schema, model): + # below addresses the draft-04 issue until https://github.com/samuelcolvin/pydantic/issues/1478 . + schema["$schema"] = qcschema_draft + def __init__(self, orient: bool = False, validate: Optional[bool] = None, **kwargs: Any) -> None: """Initializes the molecule object from dictionary-like values. @@ -1042,7 +1082,7 @@ def nuclear_repulsion_energy(self, ifr: int = None) -> float: Nuclear repulsion energy in entire molecule or in fragment. """ - Zeff = [z * int(real) for z, real in zip(self.atomic_numbers, self.real)] + Zeff = [z * int(real) for z, real in zip(cast(Iterable[int], self.atomic_numbers), self.real)] atoms = list(range(self.geometry.shape[0])) if ifr is not None: @@ -1068,7 +1108,7 @@ def nelectrons(self, ifr: int = None) -> int: Number of electrons in entire molecule or in fragment. """ - Zeff = [z * int(real) for z, real in zip(self.atomic_numbers, self.real)] + Zeff = [z * int(real) for z, real in zip(cast(Iterable[int], self.atomic_numbers), self.real)] if ifr is None: nel = sum(Zeff) - self.molecular_charge @@ -1146,7 +1186,7 @@ def align( runiq = np.asarray( [ hashlib.sha1((sym + str(mas)).encode("utf-8")).hexdigest() - for sym, mas in zip(ref_mol.symbols, ref_mol.masses) + for sym, mas in zip(cast(Iterable[str], ref_mol.symbols), ref_mol.masses) ] ) concern_mol = self @@ -1157,7 +1197,7 @@ def align( cuniq = np.asarray( [ hashlib.sha1((sym + str(mas)).encode("utf-8")).hexdigest() - for sym, mas in zip(concern_mol.symbols, concern_mol.masses) + for sym, mas in zip(cast(Iterable[str], concern_mol.symbols), concern_mol.masses) ] ) @@ -1293,7 +1333,7 @@ def scramble( runiq = np.asarray( [ hashlib.sha1((sym + str(mas)).encode("utf-8")).hexdigest() - for sym, mas in zip(ref_mol.symbols, ref_mol.masses) + for sym, mas in zip(cast(Iterable[str], ref_mol.symbols), ref_mol.masses) ] ) nat = rgeom.shape[0] diff --git a/qcelemental/models/results.py b/qcelemental/models/results.py index 92bf4652..2f79a5ec 100644 --- a/qcelemental/models/results.py +++ b/qcelemental/models/results.py @@ -1,11 +1,12 @@ from enum import Enum +from functools import partial from typing import TYPE_CHECKING, Any, Dict, Optional, Set, Union import numpy as np from pydantic import Field, constr, validator from ..util import provenance_stamp -from .basemodels import ProtoModel +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 @@ -50,7 +51,7 @@ class AtomicResultProperties(ProtoModel): None, description="The dispersion correction appended to an underlying functional when a DFT-D method is requested.", ) - scf_dipole_moment: Optional[Array[float]] = Field(None, description="The X, Y, and Z dipole components.") + scf_dipole_moment: Optional[Array[float]] = Field(None, description="The SCF X, Y, and Z dipole components.") scf_quadrupole_moment: Optional[Array[float]] = Field( None, description="The (3, 3) quadrupole components (redundant; 6 unique)." ) @@ -61,11 +62,12 @@ class AtomicResultProperties(ProtoModel): # MP2 Keywords mp2_same_spin_correlation_energy: Optional[float] = Field( - None, description="The portion of MP2 doubles correlation energy from same-spin (i.e. triplet) correlations." + None, + description="The portion of MP2 doubles correlation energy from same-spin (i.e. triplet) correlations, without any user scaling.", ) mp2_opposite_spin_correlation_energy: Optional[float] = Field( None, - description="The portion of MP2 doubles correlation energy from opposite-spin (i.e. singlet) correlations.", + description="The portion of MP2 doubles correlation energy from opposite-spin (i.e. singlet) correlations, without any user scaling.", ) mp2_singles_energy: Optional[float] = Field( None, description="The singles portion of the MP2 correlation energy. Zero except in ROHF." @@ -85,11 +87,12 @@ class AtomicResultProperties(ProtoModel): # CCSD Keywords ccsd_same_spin_correlation_energy: Optional[float] = Field( - None, description="The portion of CCSD doubles correlation energy from same-spin (i.e. triplet) correlations." + None, + description="The portion of CCSD doubles correlation energy from same-spin (i.e. triplet) correlations, without any user scaling.", ) ccsd_opposite_spin_correlation_energy: Optional[float] = Field( None, - description="The portion of CCSD doubles correlation energy from opposite-spin (i.e. singlet) correlations", + description="The portion of CCSD doubles correlation energy from opposite-spin (i.e. singlet) correlations, without any user scaling.", ) ccsd_singles_energy: Optional[float] = Field( None, description="The singles portion of the CCSD correlation energy. Zero except in ROHF." @@ -114,6 +117,26 @@ class AtomicResultProperties(ProtoModel): None, description="The CCSD(T) X, Y, and Z dipole components." ) + # CCSDT keywords + ccsdt_correlation_energy: Optional[float] = Field(None, description="The CCSDT correlation energy.") + ccsdt_total_energy: Optional[float] = Field( + None, description="The total CCSDT energy (CCSDT correlation energy + HF energy)." + ) + ccsdt_dipole_moment: Optional[Array[float]] = Field(None, description="The CCSDT X, Y, and Z dipole components.") + ccsdt_iterations: Optional[int] = Field( + None, description="The number of CCSDT iterations taken before convergence." + ) + + # CCSDTQ keywords + ccsdtq_correlation_energy: Optional[float] = Field(None, description="The CCSDTQ correlation energy.") + ccsdtq_total_energy: Optional[float] = Field( + None, description="The total CCSDTQ energy (CCSDTQ correlation energy + HF energy)." + ) + ccsdtq_dipole_moment: Optional[Array[float]] = Field(None, description="The CCSDTQ X, Y, and Z dipole components.") + ccsdtq_iterations: Optional[int] = Field( + None, description="The number of CCSDTQ iterations taken before convergence." + ) + class Config(ProtoModel.Config): force_skip_defaults = True @@ -146,6 +169,7 @@ def dict(self, *args, **kwargs): class WavefunctionProperties(ProtoModel): + """Wavefunction properties resulting from a computation. Matrix quantities are stored in column-major order.""" # Class properties _return_results_names: Set[str] = { @@ -171,42 +195,119 @@ class WavefunctionProperties(ProtoModel): ) # Core Hamiltonian - h_core_a: Optional[Array[float]] = Field(None, description="Alpha-spin core (one-electron) Hamiltonian.") - h_core_b: Optional[Array[float]] = Field(None, description="Beta-spin core (one-electron) Hamiltonian.") + h_core_a: Optional[Array[float]] = Field( + None, description="Alpha-spin core (one-electron) Hamiltonian in the AO basis.", shape=["nao", "nao"] + ) + h_core_b: Optional[Array[float]] = Field( + None, description="Beta-spin core (one-electron) Hamiltonian in the AO basis.", shape=["nao", "nao"] + ) h_effective_a: Optional[Array[float]] = Field( - None, description="Alpha-spin effective core (one-electron) Hamiltonian." + None, description="Alpha-spin effective core (one-electron) Hamiltonian in the AO basis.", shape=["nao", "nao"] ) h_effective_b: Optional[Array[float]] = Field( - None, description="Beta-spin effective core (one-electron) Hamiltonian " + None, description="Beta-spin effective core (one-electron) Hamiltonian in the AO basis", shape=["nao", "nao"] ) # SCF Results - scf_orbitals_a: Optional[Array[float]] = Field(None, description="SCF alpha-spin orbitals.") - scf_orbitals_b: Optional[Array[float]] = Field(None, description="SCF beta-spin orbitals.") - scf_density_a: Optional[Array[float]] = Field(None, description="SCF alpha-spin density matrix.") - scf_density_b: Optional[Array[float]] = Field(None, description="SCF beta-spin density matrix.") - scf_fock_a: Optional[Array[float]] = Field(None, description="SCF alpha-spin Fock matrix.") - scf_fock_b: Optional[Array[float]] = Field(None, description="SCF beta-spin Fock matrix.") - scf_eigenvalues_a: Optional[Array[float]] = Field(None, description="SCF alpha-spin eigenvalues.") - scf_eigenvalues_b: Optional[Array[float]] = Field(None, description="SCF beta-spin eigenvalues.") - scf_occupations_a: Optional[Array[float]] = Field(None, description="SCF alpha-spin occupations.") - scf_occupations_b: Optional[Array[float]] = Field(None, description="SCF beta-spin occupations.") + scf_orbitals_a: Optional[Array[float]] = Field( + None, description="SCF alpha-spin orbitals in the AO basis.", shape=["nao", "nmo"] + ) + scf_orbitals_b: Optional[Array[float]] = Field( + None, description="SCF beta-spin orbitals in the AO basis.", shape=["nao", "nmo"] + ) + scf_density_a: Optional[Array[float]] = Field( + None, description="SCF alpha-spin density matrix in the AO basis.", shape=["nao", "nao"] + ) + scf_density_b: Optional[Array[float]] = Field( + None, description="SCF beta-spin density matrix in the AO basis.", shape=["nao", "nao"] + ) + scf_fock_a: Optional[Array[float]] = Field( + None, description="SCF alpha-spin Fock matrix in the AO basis.", shape=["nao", "nao"] + ) + scf_fock_b: Optional[Array[float]] = Field( + None, description="SCF beta-spin Fock matrix in the AO basis.", shape=["nao", "nao"] + ) + scf_eigenvalues_a: Optional[Array[float]] = Field( + None, description="SCF alpha-spin orbital eigenvalues.", shape=["nmo"] + ) + scf_eigenvalues_b: Optional[Array[float]] = Field( + None, description="SCF beta-spin orbital eigenvalues.", shape=["nmo"] + ) + scf_occupations_a: Optional[Array[float]] = Field( + None, description="SCF alpha-spin orbital occupations.", shape=["nmo"] + ) + scf_occupations_b: Optional[Array[float]] = Field( + None, description="SCF beta-spin orbital occupations.", shape=["nmo"] + ) + + # BELOW from qcsk + scf_coulomb_a: Optional[Array[float]] = Field( + None, description="SCF alpha-spin Coulomb matrix in the AO basis.", shape=["nao", "nao"] + ) + scf_coulomb_b: Optional[Array[float]] = Field( + None, description="SCF beta-spin Coulomb matrix in the AO basis.", shape=["nao", "nao"] + ) + scf_exchange_a: Optional[Array[float]] = Field( + None, description="SCF alpha-spin exchange matrix in the AO basis.", shape=["nao", "nao"] + ) + scf_exchange_b: Optional[Array[float]] = Field( + None, description="SCF beta-spin exchange matrix in the AO basis.", shape=["nao", "nao"] + ) + + # Localized-orbital SCF wavefunction quantities + localized_orbitals_a: Optional[Array[float]] = Field( + None, + description="Localized alpha-spin orbitals in the AO basis. All nmo orbitals are included, even if only a subset were localized.", + shape=["nao", "nmo"], + ) + localized_orbitals_b: Optional[Array[float]] = Field( + None, + description="Localized beta-spin orbitals in the AO basis. All nmo orbitals are included, even if only a subset were localized.", + shape=["nao", "nmo"], + ) + localized_fock_a: Optional[Array[float]] = Field( + None, + description="Alpha-spin Fock matrix in the localized molecular orbital basis. All nmo orbitals are included, even if only a subset were localized.", + shape=["nmo", "nmo"], + ) + localized_fock_b: Optional[Array[float]] = Field( + None, + description="Beta-spin Fock matrix in the localized molecular orbital basis. All nmo orbitals are included, even if only a subset were localized.", + shape=["nmo", "nmo"], + ) + # ABOVE from qcsk # Return results, must be defined last - orbitals_a: Optional[str] = Field(None, description="Index to the alpha-spin orbitals of the primary return.") - orbitals_b: Optional[str] = Field(None, description="Index to the beta-spin orbitals of the primary return.") - density_a: Optional[str] = Field(None, description="Index to the alpha-spin density of the primary return.") - density_b: Optional[str] = Field(None, description="Index to the beta-spin density of the primary return.") - fock_a: Optional[str] = Field(None, description="Index to the alpha-spin Fock matrix of the primary return.") - fock_b: Optional[str] = Field(None, description="Index to the beta-spin Fock matrix of the primary return.") - eigenvalues_a: Optional[str] = Field(None, description="Index to the alpha-spin eigenvalues of the primary return.") - eigenvalues_b: Optional[str] = Field(None, description="Index to the beta-spin eigenvalues of the primary return.") - occupations_a: Optional[str] = Field( + orbitals_a: Optional[str] = Field( + None, description="Index to the alpha-spin orbitals of the primary return in the AO basis." + ) + orbitals_b: Optional[str] = Field( + None, description="Index to the beta-spin orbitals of the primary return in the AO basis." + ) + density_a: Optional[str] = Field( + None, description="Index to the alpha-spin density of the primary return in the AO basis." + ) + density_b: Optional[str] = Field( + None, description="Index to the beta-spin density of the primary return in the AO basis." + ) + fock_a: Optional[str] = Field( + None, description="Index to the alpha-spin Fock matrix of the primary return in the AO basis." + ) + fock_b: Optional[str] = Field( + None, description="Index to the beta-spin Fock matrix of the primary return in the AO basis." + ) + eigenvalues_a: Optional[str] = Field( None, description="Index to the alpha-spin orbital eigenvalues of the primary return." ) - occupations_b: Optional[str] = Field( + eigenvalues_b: Optional[str] = Field( None, description="Index to the beta-spin orbital eigenvalues of the primary return." ) + occupations_a: Optional[str] = Field( + None, description="Index to the alpha-spin orbital occupations of the primary return." + ) + occupations_b: Optional[str] = Field( + None, description="Index to the beta-spin orbital occupations of the primary return." + ) class Config(ProtoModel.Config): force_skip_defaults = True @@ -290,7 +391,7 @@ class WavefunctionProtocolEnum(str, Enum): class ErrorCorrectionProtocol(ProtoModel): """Configuration for how QCEngine handles error correction - + WARNING: These protocols are currently experimental and only supported by NWChem tasks """ @@ -320,7 +421,7 @@ class AtomicResultProtocols(ProtoModel): ) stdout: bool = Field(True, description="Primary output file to keep from a Result computation") error_correction: ErrorCorrectionProtocol = Field( - ErrorCorrectionProtocol(), description="Policies for error correction" + default_factory=ErrorCorrectionProtocol, description="Policies for error correction" ) class Config: @@ -334,20 +435,34 @@ class AtomicInput(ProtoModel): """The MolSSI Quantum Chemistry Schema""" id: Optional[str] = Field(None, description="An optional ID of the ResultInput object.") - schema_name: constr(strip_whitespace=True, regex=qcschema_input_default) = qcschema_input_default # type: ignore - schema_version: int = 1 + 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.") 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.") + keywords: Dict[str, Any] = Field({}, description="The program-specific keywords to be used.") protocols: AtomicResultProtocols = Field( AtomicResultProtocols(), description=str(AtomicResultProtocols.__base_doc__) ) - extras: Dict[str, Any] = Field({}, description="Extra fields that are not part of the schema.") + extras: Dict[str, Any] = Field( + {}, + description="Extra fields not part of the schema. Used for schema development and scratch space.", + ) + + provenance: Provenance = Field( + default_factory=partial(provenance_stamp, __name__), description=str(Provenance.__base_doc__) + ) - provenance: Provenance = Field(Provenance(**provenance_stamp(__name__)), description=str(Provenance.__base_doc__)) + class Config(ProtoModel.Config): + def schema_extra(schema, model): + schema["$schema"] = qcschema_draft def __repr_args__(self) -> "ReprArgs": return [ @@ -358,13 +473,17 @@ def __repr_args__(self) -> "ReprArgs": class AtomicResult(AtomicInput): - schema_name: constr(strip_whitespace=True, regex=qcschema_output_default) = qcschema_output_default # type: ignore - + 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}." + ), + ) properties: AtomicResultProperties = Field(..., description=str(AtomicResultProperties.__base_doc__)) wavefunction: Optional[WavefunctionProperties] = Field(None, description=str(WavefunctionProperties.__base_doc__)) return_result: Union[float, Array[float], Dict[str, Any]] = Field( - ..., description="The value requested by the 'driver' attribute." + ..., description="The primary specified return requested by the 'driver' attribute." ) # type: ignore stdout: Optional[str] = Field(None, description="The standard output of the program.") diff --git a/qcelemental/molparse/chgmult.py b/qcelemental/molparse/chgmult.py index 32d53d66..7d1ff470 100644 --- a/qcelemental/molparse/chgmult.py +++ b/qcelemental/molparse/chgmult.py @@ -360,8 +360,8 @@ def validate_and_fill_chgmult( cgmp_rules.append("4") for ifr in range(nfr): cgmp_range.append( - lambda c, fc, m, fm, ifr=ifr: _sufficient_electrons_for_mult(fzel[ifr], fc[ifr], fm[ifr]) - ) # type: ignore + lambda c, fc, m, fm, ifr=ifr: _sufficient_electrons_for_mult(fzel[ifr], fc[ifr], fm[ifr]) # type: ignore + ) cgmp_rules.append("4-" + str(ifr)) # * (R5) require total parity consistent among neutral_electrons, chg, and mult diff --git a/qcelemental/molparse/to_string.py b/qcelemental/molparse/to_string.py index a83c8364..ff0b6d8d 100644 --- a/qcelemental/molparse/to_string.py +++ b/qcelemental/molparse/to_string.py @@ -384,7 +384,7 @@ def to_dict(self) -> Dict: atom_format = "{elem}" ghost_format = "@{elem}" - umap = {"bohr": True, "angstrom": False} + umap = {"bohr": "True", "angstrom": "False"} atoms = _atoms_formatter(molrec, geom, atom_format, ghost_format, width, prec, 2) diff --git a/qcelemental/molutil/molecular_formula.py b/qcelemental/molutil/molecular_formula.py index 3fee6ca4..7ff1d8fd 100644 --- a/qcelemental/molutil/molecular_formula.py +++ b/qcelemental/molutil/molecular_formula.py @@ -1,6 +1,6 @@ import collections import re -from typing import List +from typing import Dict, List def order_molecular_formula(formula: str, order: str = "alphabetical") -> str: @@ -23,7 +23,7 @@ def order_molecular_formula(formula: str, order: str = "alphabetical") -> str: matches = re.findall(r"[A-Z][^A-Z]*", formula) if not "".join(matches) == formula: raise ValueError(f"{formula} is not a valid molecular formula.") - count = collections.defaultdict(int) + count: Dict[str, int] = collections.defaultdict(int) for match in matches: match_n = re.match(r"(\D+)(\d*)", match) assert match_n diff --git a/qcelemental/molutil/test_molutil.py b/qcelemental/molutil/test_molutil.py index 4b826f5e..bf8cf012 100644 --- a/qcelemental/molutil/test_molutil.py +++ b/qcelemental/molutil/test_molutil.py @@ -8,7 +8,7 @@ import qcelemental as qcel from qcelemental.testing import compare, compare_molrecs, compare_recursive, compare_values -from ..tests.addons import using_networkx +from ..tests.addons import drop_qcsk, using_networkx pp = pprint.PrettyPrinter(width=120) @@ -44,8 +44,9 @@ def test_scramble_descrambles_plain(): s22_12.scramble(do_shift=True, do_rotate=True, do_resort=True, do_plot=False, verbose=0, do_test=True) -def test_relative_geoms_align_free(): +def test_relative_geoms_align_free(request): s22_12 = qcel.models.Molecule.from_data(ss22_12) + drop_qcsk(s22_12, request.node.name) for trial in range(3): cmol, _ = s22_12.scramble( @@ -57,8 +58,9 @@ def test_relative_geoms_align_free(): assert compare_molrecs(rmolrec, cmolrec, atol=1.0e-4, relative_geoms="align") -def test_relative_geoms_align_fixed(): +def test_relative_geoms_align_fixed(request): s22_12 = qcel.models.Molecule.from_data(ss22_12 + "nocom\nnoreorient\n") + drop_qcsk(s22_12, request.node.name) for trial in range(3): cmol, _ = s22_12.scramble( @@ -344,7 +346,7 @@ def test_scramble_specific(): assert compare(mill_str, mill.pretty_print()) -def test_hessian_align(): +def test_hessian_align(request): # from Psi4 test test_hessian_vs_cfour[HOOH_TS-H_analytic] # fmt: off @@ -458,6 +460,7 @@ def test_hessian_align(): p4mol = qcel.models.Molecule.from_data(p4_hooh_xyz) c4mol = qcel.models.Molecule.from_data(c4_hooh_xyz) + drop_qcsk(c4mol, request.node.name) aqmol, data = p4mol.align(c4mol, atoms_map=True, mols_align=True, verbose=4) mill = data["mill"] diff --git a/qcelemental/physical_constants/context.py b/qcelemental/physical_constants/context.py index 55896d43..c2c1ffb6 100644 --- a/qcelemental/physical_constants/context.py +++ b/qcelemental/physical_constants/context.py @@ -269,8 +269,7 @@ def get(self, physical_constant: str, return_tuple: bool = False) -> Union[float # me 'electron mass' = 9.10938215E-31 # Electron rest mass (in kg) def Quantity(self, data: str) -> "quantity._Quantity": - """Returns a Pint Quantity. - """ + """Returns a Pint Quantity.""" return self.ureg.Quantity(data) diff --git a/qcelemental/physical_constants/ureg.py b/qcelemental/physical_constants/ureg.py index 05d1727c..f8106e0a 100644 --- a/qcelemental/physical_constants/ureg.py +++ b/qcelemental/physical_constants/ureg.py @@ -129,8 +129,7 @@ def build_units_registry(context): # Add contexts def _find_nist_unit(unit): - """Converts pint datatypes to NIST datatypes - """ + """Converts pint datatypes to NIST datatypes""" for value in unit.to_tuple()[1]: if value[1] < 1: continue diff --git a/qcelemental/testing.py b/qcelemental/testing.py index 7899b7e4..de6bc821 100644 --- a/qcelemental/testing.py +++ b/qcelemental/testing.py @@ -11,8 +11,7 @@ def _handle_return(passfail: bool, label: str, message: str, return_message: bool, quiet: bool = False): - """Function to print a '*label*...PASSED' line to log. - """ + """Function to print a '*label*...PASSED' line to log.""" if not quiet: if passfail: @@ -28,8 +27,7 @@ def _handle_return(passfail: bool, label: str, message: str, return_message: boo def tnm() -> str: - """Returns the name of the calling function, usually name of test case. - """ + """Returns the name of the calling function, usually name of test case.""" return sys._getframe().f_back.f_code.co_name @@ -480,11 +478,7 @@ def compare_molrecs( return_message: bool = False, return_handler: Callable = None, ) -> bool: - """Function to compare Molecule dictionaries. Prints -# :py:func:`util.success` when elements of `computed` match elements of -# `expected` to `tol` number of digits (for float arrays). - - """ + """Function to compare Molecule dictionaries.""" # Need to manipulate the dictionaries a bit, so hold values xptd = copy.deepcopy(expected) cptd = copy.deepcopy(computed) diff --git a/qcelemental/tests/addons.py b/qcelemental/tests/addons.py index e400c5ea..fe2d9e4e 100644 --- a/qcelemental/tests/addons.py +++ b/qcelemental/tests/addons.py @@ -1,5 +1,7 @@ +import json import socket from contextlib import contextmanager +from pathlib import Path import pytest @@ -49,3 +51,20 @@ def xfail_on_pubchem_busy(): pytest.xfail("Pubchem server busy") else: raise e + + +_data_path = Path(__file__).parent.resolve() / "qcschema_instances" + + +def drop_qcsk(instance, tnm: str, schema_name: str = None): + if isinstance(instance, qcelemental.models.ProtoModel) and schema_name is None: + schema_name = type(instance).__name__ + drop = (_data_path / schema_name / tnm).with_suffix(".json") + + with open(drop, "w") as fp: + if isinstance(instance, qcelemental.models.ProtoModel): + fp.write(instance.json(exclude_unset=True, exclude_none=True)) + elif isinstance(instance, dict): + json.dump(instance, fp, sort_keys=True, indent=2) + else: + raise TypeError diff --git a/qcelemental/tests/qcschema_instances/AtomicInput/dummy b/qcelemental/tests/qcschema_instances/AtomicInput/dummy new file mode 100644 index 00000000..e69de29b diff --git a/qcelemental/tests/qcschema_instances/AtomicResult/dummy b/qcelemental/tests/qcschema_instances/AtomicResult/dummy new file mode 100644 index 00000000..e69de29b diff --git a/qcelemental/tests/qcschema_instances/AtomicResultProperties/dummy b/qcelemental/tests/qcschema_instances/AtomicResultProperties/dummy new file mode 100644 index 00000000..e69de29b diff --git a/qcelemental/tests/qcschema_instances/BasisSet/dummy b/qcelemental/tests/qcschema_instances/BasisSet/dummy new file mode 100644 index 00000000..e69de29b diff --git a/qcelemental/tests/qcschema_instances/Molecule/dummy b/qcelemental/tests/qcschema_instances/Molecule/dummy new file mode 100644 index 00000000..e69de29b diff --git a/qcelemental/tests/qcschema_instances/Provenance/dummy b/qcelemental/tests/qcschema_instances/Provenance/dummy new file mode 100644 index 00000000..e69de29b diff --git a/qcelemental/tests/qcschema_instances/README.md b/qcelemental/tests/qcschema_instances/README.md new file mode 100644 index 00000000..40e34119 --- /dev/null +++ b/qcelemental/tests/qcschema_instances/README.md @@ -0,0 +1,3 @@ +These subdirectories are populated by running the QCElemental test suite. +Files are JSON representations of QCSchema instances stored or created in the course of testing. +These in turn are checked for compliance against the exported QCSchema models in test case ``test_qcschema``. diff --git a/qcelemental/tests/test_model_general.py b/qcelemental/tests/test_model_general.py index 5adee3fd..f9f3b658 100644 --- a/qcelemental/tests/test_model_general.py +++ b/qcelemental/tests/test_model_general.py @@ -13,10 +13,13 @@ Provenance, ) +from .addons import drop_qcsk -def test_result_properties_default_skip(): + +def test_result_properties_default_skip(request): obj = AtomicResultProperties(scf_one_electron_energy="-5.0") + drop_qcsk(obj, request.node.name) assert pytest.approx(obj.scf_one_electron_energy) == -5.0 @@ -31,9 +34,10 @@ def test_result_properties_default_repr(): assert len(repr(obj)) < 100 -def test_repr_provenance(): +def test_repr_provenance(request): prov = Provenance(creator="qcel", version="v0.3.2") + drop_qcsk(prov, request.node.name) assert "qcel" in str(prov) assert "qcel" in repr(prov) @@ -54,11 +58,12 @@ def test_repr_failed_op(): ) -def test_repr_result(): +def test_repr_result(request): result = AtomicInput( **{"driver": "gradient", "model": {"method": "UFF"}, "molecule": {"symbols": ["He"], "geometry": [0, 0, 0]}} ) + drop_qcsk(result, request.node.name) assert "molecule_hash" in str(result) assert "molecule_hash" in repr(result) assert "'gradient'" in str(result) diff --git a/qcelemental/tests/test_model_results.py b/qcelemental/tests/test_model_results.py index 01db8a0f..2093c608 100644 --- a/qcelemental/tests/test_model_results.py +++ b/qcelemental/tests/test_model_results.py @@ -4,6 +4,8 @@ import qcelemental as qcel from qcelemental.models import basis +from .addons import drop_qcsk + center_data = { "bs_sto3g_h": { "electron_shells": [ @@ -20,8 +22,8 @@ { "harmonic_type": "spherical", "angular_momentum": [0], - "exponents": [130.70939, 23.808861, 6.4436089], - "coefficients": [[0.15432899, 0.53532814, 0.44463454]], + "exponents": [130.70939, "23.808861", 6.4436089], + "coefficients": [[0.15432899, "0.53532814", 0.44463454]], }, { "harmonic_type": "cartesian", @@ -154,12 +156,13 @@ def test_basis_shell_centers(center_name): assert basis.BasisCenter(**center_data[center_name]) -def test_basis_set_build(): +def test_basis_set_build(request): bas = basis.BasisSet( name="custom_basis", center_data=center_data, atom_map=["bs_sto3g_o", "bs_sto3g_h", "bs_sto3g_h", "bs_def2tzvp_zr"], ) + drop_qcsk(bas, request.node.name) assert len(bas.center_data) == 3 assert len(bas.atom_map) == 4 @@ -170,6 +173,9 @@ def test_basis_set_build(): assert es[1].is_contracted() is False assert es[2].is_contracted() + assert es[0].exponents == [130.70939, 23.808861, 6.4436089] + assert es[0].coefficients == [[0.15432899, 0.53532814, 0.44463454]] + def test_basis_electron_center_raises(): data = center_data["bs_sto3g_h"]["electron_shells"][0].copy() @@ -213,19 +219,23 @@ def test_basis_map_raises(): assert basis.BasisSet(name="custom_basis", center_data=center_data, atom_map=["something_odd"]) -def test_result_build(result_data_fixture): +def test_result_build(result_data_fixture, request): ret = qcel.models.AtomicResult(**result_data_fixture) + drop_qcsk(ret, request.node.name) assert ret.wavefunction is None -def test_result_build_wavefunction_delete(wavefunction_data_fixture): +def test_result_build_wavefunction_delete(wavefunction_data_fixture, request): del wavefunction_data_fixture["protocols"] ret = qcel.models.AtomicResult(**wavefunction_data_fixture) + drop_qcsk(ret, request.node.name) assert ret.wavefunction is None -def test_wavefunction_build(wavefunction_data_fixture): - assert qcel.models.AtomicResult(**wavefunction_data_fixture) +def test_wavefunction_build(wavefunction_data_fixture, request): + ret = qcel.models.AtomicResult(**wavefunction_data_fixture) + drop_qcsk(ret, request.node.name) + assert ret def test_wavefunction_matrix_size_error(wavefunction_data_fixture): @@ -268,7 +278,7 @@ def test_wavefunction_return_result_pointer(wavefunction_data_fixture): ("return_results", True, ["orbitals_a", "fock_a", "fock_b"], ["orbitals_a", "fock_a"]), ], ) -def test_wavefunction_protocols(protocol, restricted, provided, expected, wavefunction_data_fixture): +def test_wavefunction_protocols(protocol, restricted, provided, expected, wavefunction_data_fixture, request): wfn_data = wavefunction_data_fixture["wavefunction"] @@ -289,6 +299,7 @@ def test_wavefunction_protocols(protocol, restricted, provided, expected, wavefu wfn_data[scf_name] = np.random.rand(bas.nbf, bas.nbf) wfn = qcel.models.AtomicResult(**wavefunction_data_fixture) + drop_qcsk(wfn, request.node.name) if len(expected) == 0: assert wfn.wavefunction is None @@ -316,7 +327,7 @@ def test_optimization_trajectory_protocol(keep, indices, optimization_data_fixtu "default, defined, default_result, defined_result", [(None, None, True, None), (False, {"a": True}, False, {"a": True})], ) -def test_error_correction_protocol(default, defined, default_result, defined_result, result_data_fixture): +def test_error_correction_protocol(default, defined, default_result, defined_result, result_data_fixture, request): policy = {} if default is not None: policy["default_policy"] = default @@ -324,6 +335,7 @@ def test_error_correction_protocol(default, defined, default_result, defined_res policy["policies"] = defined result_data_fixture["protocols"] = {"error_correction": policy} res = qcel.models.AtomicResult(**result_data_fixture) + drop_qcsk(res, request.node.name) assert res.protocols.error_correction.default_policy == default_result assert res.protocols.error_correction.policies == defined_result @@ -348,18 +360,20 @@ def test_error_correction_logic(): assert correction_policy.allows("a") -def test_result_build_stdout_delete(result_data_fixture): +def test_result_build_stdout_delete(result_data_fixture, request): result_data_fixture["protocols"] = {"stdout": False} ret = qcel.models.AtomicResult(**result_data_fixture) + drop_qcsk(ret, request.node.name) assert ret.stdout is None -def test_result_build_stdout(result_data_fixture): +def test_result_build_stdout(result_data_fixture, request): ret = qcel.models.AtomicResult(**result_data_fixture) + drop_qcsk(ret, request.node.name) assert ret.stdout == "I ran." -def test_failed_operation(result_data_fixture): +def test_failed_operation(result_data_fixture, request): water = qcel.models.Molecule.from_data( """ O 0 0 0 @@ -367,6 +381,7 @@ def test_failed_operation(result_data_fixture): H 0 2 0 """ ) + drop_qcsk(water, request.node.name) failed = qcel.models.FailedOperation( extras={"garbage": water}, @@ -380,12 +395,13 @@ def test_failed_operation(result_data_fixture): assert "its all good" in failed_json -def test_result_properties_array(): +def test_result_properties_array(request): lquad = [1, 2, 3, 2, 4, 5, 3, 5, 6] obj = qcel.models.AtomicResultProperties( scf_one_electron_energy="-5.0", scf_dipole_moment=[1, 2, 3], scf_quadrupole_moment=lquad ) + drop_qcsk(obj, request.node.name) assert pytest.approx(obj.scf_one_electron_energy) == -5.0 assert obj.scf_dipole_moment.shape == (3,) diff --git a/qcelemental/tests/test_molecule.py b/qcelemental/tests/test_molecule.py index 41e83360..aa56b0d7 100644 --- a/qcelemental/tests/test_molecule.py +++ b/qcelemental/tests/test_molecule.py @@ -651,14 +651,14 @@ def test_show(): def test_molecule_connectivity(): data = {"geometry": np.random.rand(5, 3), "symbols": ["he"] * 5, "validate": False} - mol = Molecule(**data, connectivity=None) + Molecule(**data, connectivity=None) connectivity = [[n, n + 1, 1] for n in range(4)] - mol = Molecule(**data, connectivity=connectivity) + Molecule(**data, connectivity=connectivity) connectivity[0][0] = -1 with pytest.raises(ValueError): - mol = Molecule(**data, connectivity=connectivity) + Molecule(**data, connectivity=connectivity) def test_orient_nomasses(): @@ -719,7 +719,7 @@ def test_sparse_molecule_connectivity(): def test_bad_isotope_spec(): - with pytest.raises(NotAnElementError) as e: + with pytest.raises(NotAnElementError): qcel.models.Molecule(symbols=["He3"], geometry=[0, 0, 0]) diff --git a/qcelemental/tests/test_molparse_from_schema.py b/qcelemental/tests/test_molparse_from_schema.py index c452509b..5d65ce8b 100644 --- a/qcelemental/tests/test_molparse_from_schema.py +++ b/qcelemental/tests/test_molparse_from_schema.py @@ -6,6 +6,8 @@ import qcelemental as qcel from qcelemental.testing import compare_molrecs +from .addons import drop_qcsk + _schema_prov_stamp = {"creator": "QCElemental", "version": "1.0", "routine": "qcelemental.molparse.from_schema"} @@ -131,9 +133,10 @@ def test_from_schema_1p5_14e(): assert compare_molrecs(schema14_psi4_np, ans, 4) -def test_from_schema_2_14e(): +def test_from_schema_2_14e(request): schema = copy.deepcopy(schema14_1) schema.update({"schema_name": "qcschema_molecule", "schema_version": 2}) + drop_qcsk(schema, request.node.name, "Molecule") ans = qcel.molparse.from_schema(schema) assert compare_molrecs(schema14_psi4_np, ans, 4) diff --git a/qcelemental/tests/test_molparse_to_schema.py b/qcelemental/tests/test_molparse_to_schema.py index c64c9653..72cdc39a 100644 --- a/qcelemental/tests/test_molparse_to_schema.py +++ b/qcelemental/tests/test_molparse_to_schema.py @@ -7,6 +7,8 @@ import qcelemental from qcelemental.testing import compare_molrecs +from .addons import drop_qcsk + _string_prov_stamp = {"creator": "QCElemental", "version": "1.0", "routine": "qcelemental.molparse.from_string"} _schema_prov_stamp = {"creator": "QCElemental", "version": "1.0", "routine": "qcelemental.molparse.from_schema"} @@ -58,12 +60,13 @@ } -def test_1_14a(): +def test_1_14a(request): fullans = copy.deepcopy(schema14_1) fullans["molecule"]["provenance"] = _string_prov_stamp final = qcelemental.molparse.from_string(subject14) kmol = qcelemental.molparse.to_schema(final["qm"], dtype=1) + drop_qcsk(kmol["molecule"], request.node.name, "Molecule") assert compare_molrecs(fullans["molecule"], kmol["molecule"]) fullans = copy.deepcopy(schema14_psi4) @@ -74,12 +77,13 @@ def test_1_14a(): assert compare_molrecs(fullans, molrec) -def test_2_14b(): +def test_2_14b(request): fullans = copy.deepcopy(schema14_2) fullans["provenance"] = _string_prov_stamp final = qcelemental.molparse.from_string(subject14) kmol = qcelemental.molparse.to_schema(final["qm"], dtype=2) + drop_qcsk(kmol, request.node.name, "Molecule") assert compare_molrecs(fullans, kmol) fullans = copy.deepcopy(schema14_psi4) @@ -109,7 +113,7 @@ def test_dtype_error(): @pytest.mark.parametrize("dtype", [1, 2]) -def test_qcschema_ang_error(dtype): +def test_atomic_units_qcschema_ang_error(dtype): final = qcelemental.molparse.from_string(subject14) with pytest.raises(qcelemental.ValidationError) as e: @@ -182,13 +186,14 @@ def test_psi4_nm_error(): } -def test_1_15a(): +def test_1_15a(request): fullans = copy.deepcopy(schema15_1) fullans["molecule"]["provenance"] = _string_prov_stamp final = qcelemental.molparse.from_string(subject15) final["qm"]["comment"] = "I has a comment" kmol = qcelemental.molparse.to_schema(final["qm"], dtype=1) + drop_qcsk(kmol["molecule"], request.node.name, "Molecule") assert compare_molrecs(fullans["molecule"], kmol["molecule"]) fullans = copy.deepcopy(schema15_psi4) @@ -201,13 +206,14 @@ def test_1_15a(): assert compare_molrecs(fullans, molrec) -def test_2_15b(): +def test_2_15b(request): fullans = copy.deepcopy(schema15_2) fullans["provenance"] = _string_prov_stamp final = qcelemental.molparse.from_string(subject15) final["qm"]["comment"] = "I has a comment" kmol = qcelemental.molparse.to_schema(final["qm"], dtype=2) + drop_qcsk(kmol, request.node.name, "Molecule") assert compare_molrecs(fullans, kmol) fullans = copy.deepcopy(schema15_psi4) @@ -280,7 +286,7 @@ def test_psi4_15c(): } -def test_froto_1_16a(): +def test_froto_1_16a(request): basic = { "schema_name": "qc_schema_output", "schema_version": 1, @@ -296,10 +302,11 @@ def test_froto_1_16a(): fullans["molecule"]["provenance"] = _schema_prov_stamp roundtrip = qcelemental.molparse.to_schema(qcelemental.molparse.from_schema(basic), dtype=1) + drop_qcsk(roundtrip["molecule"], request.node.name, "Molecule") assert compare_molrecs(fullans["molecule"], roundtrip["molecule"]) -def test_froto_2_16a(): +def test_froto_2_16a(request): basic = { "schema_name": "qcschema_molecule", "schema_version": 2, @@ -313,14 +320,18 @@ def test_froto_2_16a(): fullans["provenance"] = _schema_prov_stamp roundtrip = qcelemental.molparse.to_schema(qcelemental.molparse.from_schema(basic), dtype=2) + drop_qcsk(roundtrip, request.node.name, "Molecule") assert compare_molrecs(fullans, roundtrip) @pytest.mark.parametrize("dtype", [1, 2]) -def test_tofro_16b(dtype): +def test_tofro_16b(dtype, request): fullans = copy.deepcopy(schema16_psi4) fullans["provenance"] = _schema_prov_stamp roundtrip = qcelemental.molparse.from_schema(qcelemental.molparse.to_schema(schema16_psi4, dtype=dtype)) + qcsk = qcelemental.molparse.to_schema(schema16_psi4, dtype=dtype) + qcsk = qcsk["molecule"] if dtype == 1 else qcsk + drop_qcsk(qcsk, request.node.name, "Molecule") assert compare_molrecs(fullans, roundtrip) diff --git a/qcelemental/tests/test_molparse_to_string.py b/qcelemental/tests/test_molparse_to_string.py index 2653e3c4..bef3ece5 100644 --- a/qcelemental/tests/test_molparse_to_string.py +++ b/qcelemental/tests/test_molparse_to_string.py @@ -3,6 +3,8 @@ import qcelemental as qcel from qcelemental.testing import compare +from .addons import drop_qcsk + # CODATA2014 = 1.05835442134 # CODATA2018 = 1.058354421806 au2 = 2.0 * qcel.constants.bohr2angstroms @@ -295,9 +297,10 @@ def test_to_string_xyz(inp, expected): ("subject2", {"dtype": "nglview-sdf"}, "ans2_ngslviewsdf"), ], ) -def test_molecule_to_string(inp, kwargs, expected): +def test_molecule_to_string(inp, kwargs, expected, request): smol = _molecule_inputs[inp].to_string(**kwargs) + drop_qcsk(_molecule_inputs[inp], request.node.name) assert compare(_molecule_outputs[expected], smol) diff --git a/qcelemental/tests/test_zqcschema.py b/qcelemental/tests/test_zqcschema.py new file mode 100644 index 00000000..6d314b8d --- /dev/null +++ b/qcelemental/tests/test_zqcschema.py @@ -0,0 +1,36 @@ +import json + +import pytest + +import qcelemental as qcel + +from .addons import _data_path + + +@pytest.fixture(scope="module") +def qcschema_models(): + return {md.__name__: json.loads(md.schema_json()) for md in qcel.models.qcschema_models()} + + +files = sorted(_data_path.rglob("*.json")) +ids = [fl.parent.stem + "_" + fl.stem[5:] for fl in files] + + +@pytest.mark.parametrize("fl", files, ids=ids) +def test_qcschema(fl, qcschema_models): + import jsonschema + + model = fl.parent.stem + instance = json.loads(fl.read_text()) + + res = jsonschema.validate(instance, qcschema_models[model]) + assert res is None + + +# import pprint +# print("\n\n<<< SCHEMA") +# pprint.pprint(schemas["BasisSet"]) +# print("\n\n<<< INSTANCE") +# pprint.pprint(instance) + +# assert 0 diff --git a/qcelemental/util/gph_uno_bipartite.py b/qcelemental/util/gph_uno_bipartite.py index 4b7f28fb..41f639b5 100644 --- a/qcelemental/util/gph_uno_bipartite.py +++ b/qcelemental/util/gph_uno_bipartite.py @@ -45,10 +45,10 @@ def _formDirected(g, match): Returns ------- networkx.DiGraph - Directed graph, with edges in `match` pointing from set-0 - (bipartite attribute==0) to set-1 (bipartite attrbiute==1), and - the other edges in `g` but not in `match` pointing from set-1 to - set-0. + Directed graph, with edges in `match` pointing from set-0 + (bipartite attribute==0) to set-1 (bipartite attrbiute==1), and + the other edges in `g` but not in `match` pointing from set-1 to + set-0. """ import networkx as nx @@ -127,8 +127,8 @@ def _enumMaximumMatchingIter(g, match, all_matches, add_e=None): match : List of edges forming one maximum matching of `g`. all_matches : - List, each is a list of edges forming a maximum matching of `g`. - Newly found matchings will be appended into this list. + List, each is a list of edges forming a maximum matching of `g`. + Newly found matchings will be appended into this list. add_e : tuple, optional Edge used to form subproblems. If not `None`, will be added to each newly found matchings. @@ -335,32 +335,32 @@ def _enumMaximumMatching2(g): def _enumMaximumMatchingIter2(adj, matchadj, all_matches, n1, add_e=None, check_cycle=True): """Recurively search maximum matchings. - Similar to _enumMaximumMatching but implemented using adjacency matrix - of graph for a slight speed boost. - - Parameters - ---------- -# g : -# Undirected bipartite graph. Nodes are separated by their -# 'bipartite' attribute. -# match : -# List of edges forming one maximum matching of `g`. -# all_matches : -# List, each is a list of edges forming a maximum matching of `g`. -# Newly found matchings will be appended into this list. - add_e : tuple, optional - Edge used to form subproblems. If not `None`, will be added to each - newly found matchings. - - Returns - ------- - list - Updated list of all maximum matchings. - - Author - ------ - guangzhi XU (xugzhi1987@gmail.com; guangzhi.xu@outlook.com) - Update time: 2017-05-21 20:09:06. + Similar to _enumMaximumMatching but implemented using adjacency matrix + of graph for a slight speed boost. + + Parameters + ---------- + # g : + # Undirected bipartite graph. Nodes are separated by their + # 'bipartite' attribute. + # match : + # List of edges forming one maximum matching of `g`. + # all_matches : + # List, each is a list of edges forming a maximum matching of `g`. + # Newly found matchings will be appended into this list. + add_e : tuple, optional + Edge used to form subproblems. If not `None`, will be added to each + newly found matchings. + + Returns + ------- + list + Updated list of all maximum matchings. + + Author + ------ + guangzhi XU (xugzhi1987@gmail.com; guangzhi.xu@outlook.com) + Update time: 2017-05-21 20:09:06. """ import networkx as nx diff --git a/qcelemental/util/importing.py b/qcelemental/util/importing.py index 4bd12098..cacd3b81 100644 --- a/qcelemental/util/importing.py +++ b/qcelemental/util/importing.py @@ -1,7 +1,7 @@ import os import shutil import sys -from typing import Union +from typing import List, Union def which_import( @@ -12,7 +12,7 @@ def which_import( raise_msg: str = None, package: str = None, namespace_ok: bool = False, -) -> Union[bool, None, str]: +) -> Union[bool, None, str, List[str]]: """Tests to see if a Python module is available. Returns diff --git a/setup.py b/setup.py index b88ce62a..345fc00e 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ package_data={'': [os.path.join('qcelemental', 'data', '*.json')]}, setup_requires=[] + pytest_runner, python_requires='>=3.6', - install_requires=['numpy >= 1.12.0', 'pint >= 0.10.0', 'pydantic >= 1.0.0'], + install_requires=["numpy >= 1.12.0", "pint >= 0.10.0", "pydantic >= 1.5.0"], extras_require={ 'docs': [ 'numpydoc',