Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Csse layout input_data #459

Merged
merged 13 commits into from
Nov 28, 2024
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ jobs:
#if: false
run: |
conda remove qcelemental --force
python -m pip install 'git+https://github.com/loriab/QCElemental.git@csse_layout_536a' --no-deps
python -m pip install 'git+https://github.com/loriab/QCElemental.git@csse_layout_536b' --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 Down
13 changes: 8 additions & 5 deletions docs/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,14 @@ Misc.

MUST (Unmerged)
+++++++++++++++
- When qcengine.compute() fails and forms a fop = FailedOperation (raise_error=T), with v2, `fop.input_data` will be an <>Input model (when possible; if the error was in forming the model, it'll still be a dict), not always a dict like v1.
- When <executor>.compute() fails and collects the input for processing, with v2 it now uses the <>Input model passed to the executor, not the model-or-dict passed into compute().
- The net result of the two above is that whereas fop.input_data in v1 was reliably a dict and its contents would reflect whether a model or dict was passed to qcengine.compute(), now in v2, fop.input_data is a model whenever possible (to mirror <>Result.input_data) regardless of model or dict passed to qcengine.compute(); the only case where it's a dict is if the error was in forming the model.
- DFTD3 & DFTD4 (new intf) - intercept ``v1.AtomicResult`` with ``success=False`` and ``error`` fields set from QCSchema interfaces and return ``FailedOperation``s. Someday when upstream switches to v2, request packages return FaileOp directly and use ``input_error`` rather than ``input error``.
- ``qcengine run`` learned new argument ``--return-version`` analogous to ``qcengine.compute(..., return_version=1|2)`` so CLI matches API capabilities. Note *not* ported to phasing-out ``qcengine run-procedure``.
several got properties.return_energy, retunr_gradient
If you're missing something from AtomicResult.extras, check AtomicResult.input_data.extras in case it was passed in on input
- (:pr:`459`) OpenMM gained AtomicResult.properties.return_gradient
- (:pr:`458`) When qcengine.compute() fails and forms a fop = FailedOperation (raise_error=T), with v2, `fop.input_data` will be an <>Input model (when possible; if the error was in forming the model, it'll still be a dict), not always a dict like v1.
- (:pr:`458`) When <executor>.compute() fails and collects the input for processing, with v2 it now uses the <>Input model passed to the executor, not the model-or-dict passed into compute().
- (:pr:`458`) The net result of the two above is that whereas fop.input_data in v1 was reliably a dict and its contents would reflect whether a model or dict was passed to qcengine.compute(), now in v2, fop.input_data is a model whenever possible (to mirror <>Result.input_data) regardless of model or dict passed to qcengine.compute(); the only case where it's a dict is if the error was in forming the model.
- (:pr:`458`) DFTD3 & DFTD4 (new intf) - intercept ``v1.AtomicResult`` with ``success=False`` and ``error`` fields set from QCSchema interfaces and return ``FailedOperation``s. Someday when upstream switches to v2, request packages return FaileOp directly and use ``input_error`` rather than ``input error``.
- (:pr:`458`) ``qcengine run`` learned new argument ``--return-version`` analogous to ``qcengine.compute(..., return_version=1|2)`` so CLI matches API capabilities. Note *not* ported to phasing-out ``qcengine run-procedure``.

WIP (Unmerged)
++++++++++++++
Expand Down
13 changes: 8 additions & 5 deletions qcengine/procedures/nwchem_opt/harvester.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,18 +75,21 @@ def harvest_as_atomic_result(input_model: OptimizationInput, nwout: str) -> List
provenance["module"] = module

# Format them inout an output
input_data = input_model.input_specification.model_dump()
input_data["driver"] = "gradient"
input_data["molecule"] = out_mol

output_data = {
"schema_version": 1,
"schema_version": 2,
"input_data": input_data,
"molecule": out_mol,
"driver": "gradient",
"extras": input_model.extras.copy(),
"model": input_model.input_specification.model,
"keywords": input_model.input_specification.keywords,
"extras": {},
"properties": atprop,
"provenance": provenance,
"return_result": nwgrad,
"success": True,
}
# v2: perhaps lost the OptimizationInput.extras?

# got to even out who needs plump/flat/Decimal/float/ndarray/list
# Decimal --> str preserves precision
Expand Down
3 changes: 2 additions & 1 deletion qcengine/programs/adcc.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,15 @@ def compute(self, input_model: "AtomicInput", config: "TaskConfig") -> "AtomicRe
raise UnknownError(str(e))

input_data = input_model.model_dump(encoding="json")
output_data = input_data.copy()
output_data = {"input_data": input_data, "extras": {}, "molecule": mol}
output_data["success"] = compute_success

if compute_success:
output_data["return_result"] = adcc_state.excitation_energy[0]

extract_props = input_model.driver == "properties"
qcvars = adcc_state.to_qcvars(recurse=True, properties=extract_props)
qcvars["CURRENT ENERGY"] = adcc_state.excitation_energy[0]
atprop = build_atomicproperties(qcvars)
output_data["extras"]["qcvars"] = qcvars
output_data["properties"] = atprop
Expand Down
6 changes: 4 additions & 2 deletions qcengine/programs/aimnet2.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ def compute(self, input_data: "AtomicInput", config: "TaskConfig"):
out = model(aimnet_input)

ret_data = {
"input_data": input_data,
"molecule": input_data.molecule,
"success": False,
"properties": {
"return_energy": out["energy"].item() * ureg.conversion_factor("eV", "hartree"),
Expand All @@ -101,7 +103,7 @@ def compute(self, input_data: "AtomicInput", config: "TaskConfig"):
),
"calcinfo_natom": len(input_data.molecule.atomic_numbers),
},
"extras": input_data.extras.copy(),
"extras": {},
}
# update with calculated extras
ret_data["extras"]["aimnet2"] = {
Expand All @@ -123,4 +125,4 @@ def compute(self, input_data: "AtomicInput", config: "TaskConfig"):

ret_data["success"] = True

return AtomicResult(**{**input_data.dict(), **ret_data})
return AtomicResult(**ret_data)
7 changes: 4 additions & 3 deletions qcengine/programs/cfour/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,9 +205,10 @@ def parse_output(
provenance["module"] = module

output_data = {
"schema_version": 1,
"schema_version": 2,
"input_data": input_model,
"molecule": c4mol, # overwrites with outfile Cartesians in case fix_*=F
"extras": {**input_model.extras},
"extras": {},
"native_files": {k: v for k, v in outfiles.items() if v is not None},
"properties": atprop,
"provenance": provenance,
Expand All @@ -224,4 +225,4 @@ def parse_output(
k.upper(): str(v) if isinstance(v, Decimal) else v for k, v in qcvars.items()
}

return AtomicResult(**{**input_model.dict(), **output_data})
return AtomicResult(**output_data)
6 changes: 4 additions & 2 deletions qcengine/programs/dftd3.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,9 @@ def parse_output(self, outfiles: Dict[str, str], input_model: "AtomicInput") ->
retres = retres.ravel().tolist()

output_data = {
"extras": input_model.extras,
"input_data": input_model,
"molecule": input_model.molecule,
"extras": {},
"native_files": {k: v for k, v in outfiles.items() if v is not None},
"properties": {
"return_energy": calcinfo[f"CURRENT ENERGY"],
Expand All @@ -301,7 +303,7 @@ def parse_output(self, outfiles: Dict[str, str], input_model: "AtomicInput") ->
output_data["extras"]["qcvars"]["2-BODY PAIRWISE DISPERSION CORRECTION ANALYSIS"] = D3pairs
output_data["success"] = True

return AtomicResult(**{**input_model.dict(), **output_data})
return AtomicResult(**output_data)


def dftd3_coeff_formatter(dashlvl: str, dashcoeff: Dict) -> str:
Expand Down
31 changes: 16 additions & 15 deletions qcengine/programs/dftd_ng.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from qcelemental.util import parse_version, safe_version, which_import

from ..config import TaskConfig
from ..exceptions import InputError
from ..exceptions import InputError, ResourceError
from .empirical_dispersion_resources import from_arrays, get_dispersion_aliases
from .model import ProgramHarness

Expand Down Expand Up @@ -105,16 +105,16 @@ def compute(self, input_model: AtomicInput, config: TaskConfig) -> AtomicResult:
input_data["keywords"]["level_hint"] = level_hint

# dftd4 speaks qcsk.v1
input_model = qcelemental.models.v1.AtomicInput(**input_data)
input_model_v1 = qcelemental.models.v1.AtomicInput(**input_data)

# Run the Harness
output = run_qcschema(input_model)
output_v1 = run_qcschema(input_model_v1)

# d4 qcschema interface stores error in Result model
if not output.success:
return FailedOperation(input_data=input_data, error=output.error.model_dump())
if not output_v1.success:
return FailedOperation(input_data=input_data, error=output_v1.error.model_dump())

output = output.convert_v(2)
output = output_v1.convert_v(2, external_input_data=input_model)

if "info" in output.extras:
qcvkey = output.extras["info"]["fctldash"].upper()
Expand All @@ -126,14 +126,14 @@ def compute(self, input_model: AtomicInput, config: TaskConfig) -> AtomicResult:
if qcvkey:
calcinfo[f"{qcvkey} DISPERSION CORRECTION ENERGY"] = energy

if output.driver == "gradient":
if output.input_data.driver == "gradient":
gradient = output.return_result
calcinfo["CURRENT GRADIENT"] = gradient
calcinfo["DISPERSION CORRECTION GRADIENT"] = gradient
if qcvkey:
calcinfo[f"{qcvkey} DISPERSION CORRECTION GRADIENT"] = gradient

if output.keywords.get("pair_resolved", False):
if output.input_data.keywords.get("pair_resolved", False):
pw2 = output.extras["dftd4"]["additive pairwise energy"]
pw3 = output.extras["dftd4"]["non-additive pairwise energy"]
assert abs(pw2.sum() + pw3.sum() - energy) < 1.0e-8, f"{pw2.sum()} + {pw3.sum()} != {energy}"
Expand Down Expand Up @@ -279,16 +279,17 @@ def compute(self, input_model: AtomicInput, config: TaskConfig) -> AtomicResult:
input_data["keywords"]["params_tweaks"] = {**planinfo["dashparams"], "s9": 0.0}
input_data["keywords"]["level_hint"] = level_hint

input_model = qcelemental.models.v1.AtomicInput(**input_data)
# sdftd3 speaks qcsk.v1
input_model_v1 = qcelemental.models.v1.AtomicInput(**input_data)

# Run the Harness
output = run_qcschema(input_model)
output_v1 = run_qcschema(input_model_v1)

# d3 qcschema interface stores error in Result model
if not output.success:
return FailedOperation(input_data=input_data, error=output.error.model_dump())
if not output_v1.success:
return FailedOperation(input_data=input_data, error=output_v1.error.model_dump())

output = output.convert_v(2)
output = output_v1.convert_v(2, external_input_data=input_model)

if "info" in output.extras:
qcvkey = output.extras["info"]["fctldash"].upper()
Expand All @@ -300,14 +301,14 @@ def compute(self, input_model: AtomicInput, config: TaskConfig) -> AtomicResult:
if qcvkey:
calcinfo[f"{qcvkey} DISPERSION CORRECTION ENERGY"] = energy

if output.driver == "gradient":
if output.input_data.driver == "gradient":
gradient = output.return_result
calcinfo["CURRENT GRADIENT"] = gradient
calcinfo["DISPERSION CORRECTION GRADIENT"] = gradient
if qcvkey:
calcinfo[f"{qcvkey} DISPERSION CORRECTION GRADIENT"] = gradient

if output.keywords.get("pair_resolved", False):
if output.input_data.keywords.get("pair_resolved", False):
pw2 = output.extras["dftd3"]["additive pairwise energy"]
pw3 = output.extras["dftd3"]["non-additive pairwise energy"]
assert abs(pw2.sum() + pw3.sum() - energy) < 1.0e-8, f"{pw2.sum()} + {pw3.sum()} != {energy}"
Expand Down
7 changes: 4 additions & 3 deletions qcengine/programs/gamess/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,9 +253,10 @@ def parse_output(self, outfiles: Dict[str, str], input_model: AtomicInput) -> At
provenance["module"] = module

output_data = {
"schema_version": 1,
"schema_version": 2,
"input_data": input_model,
"molecule": gamessmol, # overwrites with outfile Cartesians in case fix_*=F
"extras": {**input_model.extras},
"extras": {},
"native_files": {k: v for k, v in outfiles.items() if v is not None},
"properties": atprop,
"provenance": provenance,
Expand All @@ -271,7 +272,7 @@ def parse_output(self, outfiles: Dict[str, str], input_model: AtomicInput) -> At
k.upper(): str(v) if isinstance(v, Decimal) else v for k, v in qcvars.items()
}

return AtomicResult(**{**input_model.dict(), **output_data})
return AtomicResult(**output_data)

@staticmethod
def _partition(total: float, fraction_replicated: float, ncores: int) -> Tuple[int, int]:
Expand Down
14 changes: 11 additions & 3 deletions qcengine/programs/gcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,10 +261,18 @@ def parse_output(self, outfiles: Dict[str, str], input_model: "AtomicInput") ->
elif isinstance(retres, np.ndarray):
retres = retres.ravel().tolist()

properties = {
"calcinfo_natom": len(input_model.molecule.symbols),
"return_energy": ene,
f"return_{input_model.driver.value}": retres,
}

output_data = {
"extras": input_model.extras,
"input_data": input_model,
"molecule": input_model.molecule,
"extras": {},
"native_files": {k: v for k, v in outfiles.items() if v is not None},
"properties": {},
"properties": properties,
"provenance": Provenance(
creator="GCP", version=self.get_version(), routine=__name__ + "." + sys._getframe().f_code.co_name
),
Expand All @@ -276,7 +284,7 @@ def parse_output(self, outfiles: Dict[str, str], input_model: "AtomicInput") ->
output_data["extras"]["qcvars"] = calcinfo

output_data["success"] = True
return AtomicResult(**{**input_model.dict(), **output_data})
return AtomicResult(**output_data)


class MCTCGCPHarness(GCPHarness):
Expand Down
5 changes: 3 additions & 2 deletions qcengine/programs/mace.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,11 @@ def compute(self, input_data: "AtomicInput", config: "TaskConfig") -> Union["Ato
else:
raise InputError("MACE only supports the energy and gradient driver methods.")

ret_data["extras"] = input_data.extras.copy()
ret_data["input_data"] = input_data
ret_data["molecule"] = input_data.molecule
ret_data["provenance"] = Provenance(creator="mace", version=mace.__version__, routine="mace")
ret_data["schema_name"] = "qcschema_output"
ret_data["success"] = True

# Form up a dict first, then sent to BaseModel to avoid repeat kwargs which don't override each other
return AtomicResult(**{**input_data.dict(), **ret_data})
return AtomicResult(**ret_data)
4 changes: 2 additions & 2 deletions qcengine/programs/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ def build_input_model(
# Note: Someday when the multiple QCSchema versions QCEngine supports are all within the
# Pydantic v2 API base class, this can use discriminated unions instead of logic.

v1_model = getattr(qcelemental.models.v1, "AtomicInput")
v2_model = getattr(qcelemental.models.v2, "AtomicInput")
v1_model = qcelemental.models.v1.AtomicInput
v2_model = qcelemental.models.v2.AtomicInput

if isinstance(data, v1_model):
mdl = model_wrapper(data, v1_model)
Expand Down
5 changes: 3 additions & 2 deletions qcengine/programs/molpro.py
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,7 @@ def parse_output(self, outfiles: Dict[str, str], input_model: "AtomicInput") ->
raise KeyError(f"Could not find {method} total energy")

# Initialize output_data by copying over input_model.dict()
output_data = input_model.dict()
output_data = {"input_data": input_model, "molecule": input_model.molecule} # TODO better mol?

# Determining return_result
if input_model.driver == "energy":
Expand All @@ -442,9 +442,10 @@ def parse_output(self, outfiles: Dict[str, str], input_model: "AtomicInput") ->

# Final output_data assignments needed for the AtomicResult object
output_data["properties"] = properties
output_data["extras"].update(extras)
output_data["extras"] = extras
output_data["schema_name"] = "qcschema_output"
output_data["stdout"] = outfiles["dispatch.out"]
output_data["success"] = True
output_data["provenance"] = input_model.provenance # TODO better stamp?

return AtomicResult(**output_data)
4 changes: 2 additions & 2 deletions qcengine/programs/mopac.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,13 +278,13 @@ def parse_output(self, outfiles: Dict[str, str], input_model: "AtomicInput") ->

gradient = data.pop("gradients")

output = input_model.dict()
output = {"input_data": input_model, "molecule": input_model.molecule}
output["provenance"] = {"creator": "mopac", "version": data.pop("mopac_version")}

output["properties"] = {}
output["properties"]["return_energy"] = data["heat_of_formation"]

output["extras"].update(data)
output["extras"] = data

if input_model.driver == "energy":
output["return_result"] = data["heat_of_formation"]
Expand Down
14 changes: 11 additions & 3 deletions qcengine/programs/mp2d.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,10 +214,18 @@ def parse_output(self, outfiles: Dict[str, str], input_model: "AtomicInput") ->
elif isinstance(retres, np.ndarray):
retres = retres.ravel().tolist()

properties = {
"calcinfo_natom": len(input_model.molecule.symbols),
"return_energy": ene,
f"return_{input_model.driver.value}": retres,
}

output_data = {
"extras": input_model.extras,
"input_data": input_model,
"molecule": input_model.molecule,
"extras": {},
"native_files": {k: v for k, v in outfiles.items() if v is not None},
"properties": {},
"properties": properties,
"provenance": Provenance(
creator="MP2D", version=self.get_version(), routine=__name__ + "." + sys._getframe().f_code.co_name
),
Expand All @@ -229,4 +237,4 @@ def parse_output(self, outfiles: Dict[str, str], input_model: "AtomicInput") ->
output_data["extras"]["qcvars"] = calcinfo

output_data["success"] = True
return AtomicResult(**{**input_model.dict(), **output_data})
return AtomicResult(**output_data)
8 changes: 3 additions & 5 deletions qcengine/programs/mrchem.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,11 @@ def compute(self, input_model: "AtomicInput", config: "TaskConfig") -> "AtomicRe
input_data = copy.deepcopy(job_input["mrchem_json"])

output_data = {
"keywords": input_data,
"schema_name": "qcschema_output",
"schema_version": 1,
"model": input_model.model,
"schema_version": 2,
"input_data": input_model,
"molecule": input_model.molecule,
"driver": input_model.driver,
"extras": input_model.extras,
"extras": {},
}

with temporary_directory(parent=parent, suffix="_mrchem_scratch") as tmpdir:
Expand Down
Loading
Loading