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 pyd2 modernize compute function #455

Open
wants to merge 7 commits into
base: next2024
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ jobs:
# label: QCore
# runs-on: ubuntu-latest
# pytest: ""
# Note: removed Sep 2024 b/c too hard to reconcile w/pyd v2
# Note: removed Sep 2024 b/c too hard to reconcile w/pyd v2. Manby approves.

- conda-env: nwchem
python-version: 3.8
Expand Down Expand Up @@ -114,6 +114,12 @@ jobs:
runs-on: ubuntu-latest
pytest: ""

#- conda-env: nwchem
# python-version: "3.10"
# label: TeraChem
# runs-on: ubuntu-20.04
# pytest: ""

name: "🐍 ${{ matrix.cfg.python-version }} • ${{ matrix.cfg.label }} • ${{ matrix.cfg.runs-on }}"
runs-on: ${{ matrix.cfg.runs-on }}

Expand Down Expand Up @@ -144,11 +150,15 @@ jobs:
run: |
qcore --accept-license


#docker.io/mtzgroup/terachem:latest
#Usage: docker exec [OPTIONS] CONTAINER COMMAND [ARG...]

- name: Special Config - QCElemental Dep
#if: false
run: |
conda remove qcelemental --force
python -m pip install 'git+https://github.com/loriab/QCElemental.git@csse_pyd2_shimclasses' --no-deps
python -m pip install 'git+https://github.com/loriab/QCElemental.git@csse_pyd2_converterclasses' --no-deps

# note: conda remove --force, not mamba remove --force b/c https://github.com/mamba-org/mamba/issues/412
# alt. is micromamba but not yet ready for setup-miniconda https://github.com/conda-incubator/setup-miniconda/issues/75
Expand All @@ -163,6 +173,12 @@ jobs:
run: |
sed -i s/from\ pydantic\ /from\ pydantic.v1\ /g ${CONDA_PREFIX}/lib/python${{ matrix.cfg.python-version }}/site-packages/psi4/driver/*py

- name: Special Config - Forced Interface Upgrade
if: "(matrix.cfg.label == 'Psi4-1.6')"
run: |
grep -r "local_options" ${CONDA_PREFIX}/lib/python${{ matrix.cfg.python-version }}/site-packages/psi4/driver/
sed -i "s/local_options/task_config/g" ${CONDA_PREFIX}/lib/python${{ matrix.cfg.python-version }}/site-packages/psi4/driver/procrouting/*py

- name: Install QCEngine
run: |
python -m pip install . --no-deps
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ repos:
- id: black
language_version: python3.12
args: [--line-length=120]
exclude: 'test_|versioneer.py|examples/.*|docs/.*|devtools/.*'
exclude: 'versioneer.py|examples/.*|docs/.*|devtools/.*'
- repo: https://github.com/pycqa/isort
rev: 5.13.2
hooks:
Expand Down
12 changes: 12 additions & 0 deletions docs/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@ Breaking Changes
- (:pr:`453`) Deps - Require pydantic v2 dependency (don't worry, this isn't
changing QCEngine's role as QCSchema I/O runner. Also require pydantic-settings
for CLI. @loriab
- (:pr:`455`) API - As promised 2 years ago for >=v0.30, `local_options` has
been removed in favor of `task_config` in `compute` and `compute_procedure`.
Note that Psi4 v1.6 will need an older qcel or a sed to work (see GHA). The
`qcengine.MDIEngine` is on notice (probably not user-facing. @loriab
- (:pr:`455`) API - `qcengine.compute` and `qcengine.compute_procedure` have been
merged in favor of the former. Also, treat the second argument (e.g., "nwchem"
or "geometric") as a positional argument, rather than keyword argument with key
"program" or "procedure". @loriab
- (:pr:`455`) API - `compute` learned an optional argument `return_version` to
specify the schema_version of the returned model or dictionary. By default it'll
return the input schema_version. If not determinable, it will return v1. @loriab

New Features
++++++++++++
Expand All @@ -45,6 +56,7 @@ Enhancements
- (:pr:`453`) Maint - Convert internal (non-QCSchema) pydantic classes to
pydantic v2 API, namely `NodeDescriptor`, `TaskConfig`, `ProgramHarness`,
`ProcedureHarness`. @loriab
- (:pr:`454`) Testing - Tests check QCSchema v1 and v2. @loriab

Bug Fixes
+++++++++
Expand Down
128 changes: 43 additions & 85 deletions qcengine/compute.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
import warnings
from typing import TYPE_CHECKING, Any, Dict, Optional, Union

from qcelemental.models import AtomicInput, AtomicResult, FailedOperation, OptimizationResult
import qcelemental
from qcelemental.models import AtomicInput, AtomicResult, FailedOperation, OptimizationResult # TODO

from .config import get_config
from .exceptions import InputError, RandomError
Expand Down Expand Up @@ -33,13 +34,15 @@ def _process_failure_and_return(model, return_dict, raise_error):


def compute(
input_data: Union[Dict[str, Any], "AtomicInput"],
input_data: Union[Dict[str, Any], "BaseModel"], # TODO Input base class
program: str,
raise_error: bool = False,
task_config: Optional[Dict[str, Any]] = None,
local_options: Optional[Dict[str, Any]] = None,
return_dict: bool = False,
) -> Union["AtomicResult", "FailedOperation", Dict[str, Any]]:
return_version: int = -1,
) -> Union[
"BaseModel", "FailedOperation", Dict[str, Any]
]: # TODO Output base class, was AtomicResult OptimizationResult
"""Executes a single CMS program given a QCSchema input.

The full specification can be found at:
Expand All @@ -50,49 +53,50 @@ def compute(
input_data
A QCSchema input specification in dictionary or model from QCElemental.models
program
The CMS program with which to execute the input.
The CMS program or procedure with which to execute the input. E.g., "psi4", "rdkit", "geometric".
raise_error
Determines if compute should raise an error or not.
retries : int, optional
The number of random tries to retry for.
task_config
A dictionary of local configuration options corresponding to a TaskConfig object.
local_options
Deprecated parameter, renamed to ``task_config``
Formerly local_options.
return_dict
Returns a dict instead of qcelemental.models.AtomicResult
Returns a dict instead of qcelemental.models.AtomicResult # TODO base Result class
return_version
The schema version to return. If -1, the input schema_version is used.

Returns
-------
result
AtomicResult, FailedOperation, or Dict representation of either object type
AtomicResult, OptimizationResult, FailedOperation, etc., or Dict representation of any object type
A QCSchema representation of the requested output, type depends on return_dict key.
"""

output_data = input_data.copy() # lgtm [py/multiple-definition]
with compute_wrapper(capture_output=False, raise_error=raise_error) as metadata:
try:
# models, v1 or v2
output_data = input_data.model_copy()
except AttributeError:
# dicts
output_data = input_data.copy() # lgtm [py/multiple-definition]

# Grab the executor and build the input model
executor = get_program(program)
with compute_wrapper(capture_output=False, raise_error=raise_error) as metadata:
# Grab the executor harness
try:
executor = get_procedure(program)
except InputError:
executor = get_program(program)

# Build the model and validate
input_data = model_wrapper(input_data, AtomicInput)
# * calls model_wrapper with the (Atomic|Optimization|etc)Input for which the harness was designed
# * upon return, input_data is a model of the type (e.g., Atomic) and version (e.g., 1 or 2) the harness prefers. for now, v1.
input_data, input_schema_version = executor.build_input_model(input_data, return_input_schema_version=True)
return_version = input_schema_version if return_version == -1 else return_version

# Build out task_config
if task_config is None:
task_config = {}

if local_options:
warnings.warn(
"Using the `local_options` keyword argument is deprecated in favor of using `task_config`, "
"in version 0.30.0 it will stop working.",
category=FutureWarning,
stacklevel=2,
)
task_config = {**local_options, **task_config}

input_engine_options = input_data.extras.pop("_qcengine_local_config", {})

task_config = {**task_config, **input_engine_options}
config = get_config(task_config=task_config)

Expand All @@ -113,66 +117,20 @@ def compute(
except:
raise

return handle_output_metadata(output_data, metadata, raise_error=raise_error, return_dict=return_dict)


def compute_procedure(
input_data: Union[Dict[str, Any], "BaseModel"],
procedure: str,
raise_error: bool = False,
task_config: Optional[Dict[str, str]] = None,
local_options: Optional[Dict[str, str]] = None,
return_dict: bool = False,
) -> Union["OptimizationResult", "FailedOperation", Dict[str, Any]]:
"""Runs a procedure (a collection of the quantum chemistry executions)

Parameters
----------
input_data : dict or qcelemental.models.OptimizationInput
A JSON input specific to the procedure executed in dictionary or model from QCElemental.models
procedure : {"geometric", "berny"}
The name of the procedure to run
raise_error : bool, option
Determines if compute should raise an error or not.
task_config
A dictionary of local configuration options corresponding to a TaskConfig object.
local_options
Deprecated parameter, renamed to ``task_config``
return_dict : bool, optional, default True
Returns a dict instead of qcelemental.models.AtomicInput

Returns
------
dict, OptimizationResult, FailedOperation
A QC Schema representation of the requested output, type depends on return_dict key.
"""
# Build out task_config
if task_config is None:
task_config = {}

if local_options:
warnings.warn(
"Using the `local_options` keyword argument is depreciated in favor of using `task_config`, "
"in version 0.30.0 it will stop working.",
category=FutureWarning,
stacklevel=2,
)
task_config = {**local_options, **task_config}

output_data = input_data.copy() # lgtm [py/multiple-definition]
with compute_wrapper(capture_output=False, raise_error=raise_error) as metadata:
return handle_output_metadata(
output_data, metadata, raise_error=raise_error, return_dict=return_dict, convert_version=return_version
)

# Grab the executor and build the input model
executor = get_procedure(procedure)

config = get_config(task_config=task_config)
input_data = executor.build_input_model(input_data)

# Create a base output data in case of errors
output_data = input_data.copy() # lgtm [py/multiple-definition]

# Set environment parameters and execute
with environ_context(config=config):
output_data = executor.compute(input_data, config)
def compute_procedure(*args, **kwargs):
vchanges = qcelemental.models.common_models._qcsk_v2_default_v1_importpathschange

return handle_output_metadata(output_data, metadata, raise_error=raise_error, return_dict=return_dict)
warnings.warn(
f"Using the `compute_procedure` function is deprecated in favor of using `compute`, "
"and as soon as version {vchanges} it will stop working.",
category=FutureWarning,
stacklevel=2,
)
if "procedure" in kwargs:
kwargs["program"] = kwargs.pop("procedure")
return compute(*args, **kwargs)
18 changes: 15 additions & 3 deletions qcengine/mdi_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def __init__(
model,
keywords,
raise_error: bool = False,
task_config: Optional[Dict[str, Any]] = None,
local_options: Optional[Dict[str, Any]] = None,
):
"""Initialize an MDIServer object for communication with MDI
Expand All @@ -59,8 +60,9 @@ def __init__(
Program-specific keywords.
raise_error : bool, optional
Determines if compute should raise an error or not.
local_options : Optional[Dict[str, Any]], optional
task_config : Optional[Dict[str, Any]], optional
A dictionary of local configuration options
Can be passed as `local_options`, but `task_config` preferred.
"""

if not use_mdi:
Expand All @@ -81,7 +83,17 @@ def __init__(
self.keywords = keywords
self.program = program
self.raise_error = raise_error
self.local_options = local_options
if task_config is None:
task_config = {}
if local_options:
warnings.warn(
"Using the `local_options` keyword argument is deprecated in favor of using `task_config`, "
"and as soon as version 0.70.0 it will stop working.",
category=FutureWarning,
stacklevel=2,
)
task_config = {**local_options, **task_config}
self.local_options = task_config

# The MDI interface does not currently support multiple fragments
if len(self.molecule.fragments) != 1:
Expand Down Expand Up @@ -320,7 +332,7 @@ def run_energy(self) -> None:
molecule=self.molecule, driver="gradient", model=self.model, keywords=self.keywords
)
self.compute_return = compute(
input_data=input, program=self.program, raise_error=self.raise_error, local_options=self.local_options
input_data=input, program=self.program, raise_error=self.raise_error, task_config=self.local_options
)

# If there is an error message, print it out
Expand Down
6 changes: 4 additions & 2 deletions qcengine/procedures/berny.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ def found(self, raise_error: bool = False) -> bool:
raise_msg="Please install via `pip install pyberny`.",
)

def build_input_model(self, data: Union[Dict[str, Any], "OptimizationInput"]) -> "OptimizationInput":
return self._build_model(data, OptimizationInput)
def build_input_model(
self, data: Union[Dict[str, Any], "OptimizationInput"], *, return_input_schema_version: bool = False
) -> "OptimizationInput":
return self._build_model(data, "OptimizationInput", return_input_schema_version=return_input_schema_version)

def compute(
self, input_data: "OptimizationInput", config: "TaskConfig"
Expand Down
6 changes: 4 additions & 2 deletions qcengine/procedures/geometric.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@ def get_version(self) -> str:

return self.version_cache[which_prog]

def build_input_model(self, data: Union[Dict[str, Any], "OptimizationInput"]) -> "OptimizationInput":
return self._build_model(data, OptimizationInput)
def build_input_model(
self, data: Union[Dict[str, Any], "OptimizationInput"], *, return_input_schema_version: bool = False
) -> "OptimizationInput":
return self._build_model(data, "OptimizationInput", return_input_schema_version=return_input_schema_version)

def compute(self, input_model: "OptimizationInput", config: "TaskConfig") -> "OptimizationResult":
try:
Expand Down
28 changes: 25 additions & 3 deletions qcengine/procedures/model.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import abc
from typing import Any, Dict, Union
from typing import Any, Dict, Tuple, Union

import qcelemental
from pydantic import BaseModel, ConfigDict

from ..util import model_wrapper
Expand Down Expand Up @@ -52,12 +53,33 @@ def found(self, raise_error: bool = False) -> bool:
If the proceudre was found or not.
"""

def _build_model(self, data: Dict[str, Any], model: "BaseModel") -> "BaseModel":
def _build_model(
self, data: Dict[str, Any], model: "BaseModel", /, *, return_input_schema_version: bool = False
) -> Union["BaseModel", Tuple["BaseModel", int]]:
"""
Quick wrapper around util.model_wrapper for inherited classes
"""

return model_wrapper(data, model)
v1_model = getattr(qcelemental.models.v1, model)
v2_model = getattr(qcelemental.models.v2, model)

if isinstance(data, v1_model):
mdl = model_wrapper(data, v1_model)
elif isinstance(data, v2_model):
mdl = model_wrapper(data, v2_model)
elif isinstance(data, dict):
# remember these are user-provided dictionaries, so they'll have the mandatory fields,
# like driver, not the helpful discriminator fields like schema_version.

# for now, the two dictionaries look the same, so cast to the one we want
# note that this prevents correctly identifying the user schema version when dict passed in, so either as_v1/None or as_v2 will fail
mdl = model_wrapper(data, v1_model) # TODO v2

input_schema_version = mdl.schema_version
if return_input_schema_version:
return mdl.convert_v(1), input_schema_version # non-psi4 return_dict=False fail w/o this
else:
return mdl.convert_v(1)

def get_version(self) -> str:
"""Finds procedure, extracts version, returns normalized version string.
Expand Down
6 changes: 4 additions & 2 deletions qcengine/procedures/nwchem_opt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ def get_version(self) -> str:
nwc_harness = NWChemHarness()
return nwc_harness.get_version()

def build_input_model(self, data: Union[Dict[str, Any], "OptimizationInput"]) -> OptimizationInput:
return self._build_model(data, OptimizationInput)
def build_input_model(
self, data: Union[Dict[str, Any], "OptimizationInput"], *, return_input_schema_version: bool = False
) -> "OptimizationInput":
return self._build_model(data, "OptimizationInput", return_input_schema_version=return_input_schema_version)

def compute(self, input_data: OptimizationInput, config: TaskConfig) -> "BaseModel":
nwc_harness = NWChemHarness()
Expand Down
Loading
Loading