diff --git a/.bumpversion.cfg b/.bumpversion.cfg index e69be12..ebe72b5 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.1.2 +current_version = 0.2.0 commit = True tag = True diff --git a/conceptual_dictionary/__init__.py b/conceptual_dictionary/__init__.py index bd26654..afc8190 100644 --- a/conceptual_dictionary/__init__.py +++ b/conceptual_dictionary/__init__.py @@ -5,6 +5,20 @@ property_template, workflow_template, operation_template, + math_operation_template, +) +from conceptual_dictionary.vocabs import ( + CONTROLLED_VALUES, + METHOD, + ALGORITHM, + DEGREES_OF_FREEDOM, + THERMODYNAMIC_ENSEMBLE, + POTENTIAL_TYPE, + XC_FUNCTIONAL, + OPERATION_METHOD, + GRAIN_BOUNDARY_TYPE, + MATH_OPERATION_TYPE, + YAML_TOP_LEVEL_KEYS, ) __all__ = [ @@ -13,4 +27,16 @@ "property_template", "workflow_template", "operation_template", + "math_operation_template", + "CONTROLLED_VALUES", + "METHOD", + "ALGORITHM", + "DEGREES_OF_FREEDOM", + "THERMODYNAMIC_ENSEMBLE", + "POTENTIAL_TYPE", + "XC_FUNCTIONAL", + "OPERATION_METHOD", + "GRAIN_BOUNDARY_TYPE", + "MATH_OPERATION_TYPE", + "YAML_TOP_LEVEL_KEYS", ] diff --git a/conceptual_dictionary/conceptualdict.py b/conceptual_dictionary/conceptualdict.py index 00d542d..fa076d5 100644 --- a/conceptual_dictionary/conceptualdict.py +++ b/conceptual_dictionary/conceptualdict.py @@ -1,14 +1,22 @@ +import warnings import yaml import json -from typing import Any, Dict +from typing import Any, Dict, List import numpy as np import string import random +from conceptual_dictionary.vocabs import CONTROLLED_VALUES + class ConceptualDict(dict): def __init__(self, *args, **kwargs): - data = {"computational_sample": [], "workflow": [], "operation": []} + data = { + "computational_sample": [], + "workflow": [], + "operation": [], + "math_operation": [], + } super().__init__(data, *args, **kwargs) def generate_id(self, length=7): @@ -68,3 +76,113 @@ def from_json(cls, filepath: str) -> "ConceptualDict": kg = cls() kg.update(data) return kg + + # -------------------- + # Validation + # -------------------- + def validate(self, strict: bool = False) -> List[dict]: + """Check all entries against controlled vocabularies sourced from atomRDF. + + Parameters + ---------- + strict : bool + If False (default), emit ``warnings.warn`` for each violation and + return the full list. If True, raise ``ValueError`` on the first + violation — useful in CI / test contexts. + + Returns + ------- + list[dict] + Each item has keys ``section``, ``index``, ``field``, ``value``, + ``allowed`` so callers can inspect violations programmatically. + """ + violations: List[dict] = [] + + def _check(section: str, idx: int, field: str, value, allowed: frozenset): + if value is None: + return + values = value if isinstance(value, list) else [value] + for v in values: + if v not in allowed: + msg = ( + f"[{section}][{idx}] '{field}' has invalid value '{v}'. " + f"Allowed: {sorted(allowed)}" + ) + violations.append( + dict( + section=section, + index=idx, + field=field, + value=v, + allowed=allowed, + ) + ) + if strict: + raise ValueError(msg) + warnings.warn(msg, UserWarning, stacklevel=2) + + for i, wf in enumerate(self.get("workflow", [])): + _check( + "workflow", + i, + "method", + wf.get("method"), + CONTROLLED_VALUES["workflow.method"][0], + ) + _check( + "workflow", + i, + "algorithm", + wf.get("algorithm"), + CONTROLLED_VALUES["workflow.algorithm"][0], + ) + _check( + "workflow", + i, + "degrees_of_freedom", + wf.get("degrees_of_freedom"), + CONTROLLED_VALUES["workflow.degrees_of_freedom"][0], + ) + _check( + "workflow", + i, + "thermodynamic_ensemble", + wf.get("thermodynamic_ensemble"), + CONTROLLED_VALUES["workflow.thermodynamic_ensemble"][0], + ) + _check( + "workflow", + i, + "xc_functional", + wf.get("xc_functional"), + CONTROLLED_VALUES["workflow.xc_functional"][0], + ) + pot = wf.get("interatomic_potential") + if isinstance(pot, dict): + _check( + "workflow", + i, + "potential_type", + pot.get("potential_type"), + CONTROLLED_VALUES["workflow.potential_type"][0], + ) + + for i, op in enumerate(self.get("operation", [])): + _check( + "operation", + i, + "method", + op.get("method"), + CONTROLLED_VALUES["operation.method"][0], + ) + + for i, mo in enumerate(self.get("math_operation", [])): + _check( + "math_operation", + i, + "type", + mo.get("type"), + CONTROLLED_VALUES["math_operation.type"][0], + ) + + return violations diff --git a/conceptual_dictionary/templates.py b/conceptual_dictionary/templates.py index 382c309..09c5129 100644 --- a/conceptual_dictionary/templates.py +++ b/conceptual_dictionary/templates.py @@ -28,15 +28,17 @@ "species": None, # Option B — file reference (preferred for large MD snapshots) # WorkflowParser resolves this relative to the YAML file's directory. - "file_path": None, # path to structure file, e.g. '../DC3_benchmark_data_set/Al_fcc/T_0.10Tm_snapshot_1.gz' + "file_path": None, # path to structure file, e.g. '../DC3_benchmark_data_set/Al_fcc/T_0.10Tm_snapshot_1.gz' "file_format": None, # ASE format string, e.g. 'lammps-dump-text' (auto-detected if None) - "file_species": None, # species order for LAMMPS numeric types, e.g. ['Al'] + "file_species": None, # species order for LAMMPS numeric types, e.g. ['Al'] }, "calculated_property": [], } property_template = { - "basename": None, + "id": None, # optional local ID; used to reference this property in math_operation operands + "label": None, # primary name read by atomRDF WorkflowParser + "basename": None, # kept for backwards compat "value": None, "unit": None, "associate_to_sample": [], @@ -46,20 +48,29 @@ "algorithm": None, "method": None, "xc_functional": None, - "input_parameter": [], + # Each entry in these lists may include an optional 'id' field so later + # math_operation entries can reference the property by its local ID. + "input_parameter": [ + # {"id": None, "label": None, "basename": None, "value": None, "unit": None} + ], "input_sample": [], "output_sample": [], - "calculated_property": [], + "output_parameter": [ + # {"id": None, "label": None, "basename": None, "value": None, "unit": None} + ], + "calculated_property": [ + # {"id": None, "label": None, "basename": None, "value": None, "unit": None, + # "associate_to_sample": []} + ], "degrees_of_freedom": [], "interatomic_potential": { "potential_type": None, "uri": None, }, - "software": { - "uri": None, - "version": None, - "label": None, - }, + # software is a list of {uri, version, label} dicts + "software": [], + # single-software template kept for reference: + # {"uri": None, "version": None, "label": None} "workflow_manager": { "uri": None, "version": None, @@ -68,6 +79,58 @@ "thermodynamic_ensemble": None, } +dataset_template = { + # dcat:Dataset — the dataset node + "identifier": None, # URI/IRI for the dataset, e.g. "https://doi.org/10.5281/zenodo.1234567" + "title": None, # dcterms:title, e.g. "Grain boundary energies for Al" + # dcterms:creator — list of foaf:Person entries + "creators": [ + { + "id": None, # URI for the person, e.g. "https://orcid.org/0000-0000-0000-0000" + "name": None, # foaf:name, e.g. "Abril Guzman" + } + ], + # dcterms:isReferencedBy — the associated publication + "publication": { + "id": None, # URI for the paper + "identifier": None, # dcterms:identifier — DOI string, e.g. "10.1016/j.actamat.2024.12345" + "title": None, # dcterms:title (optional) + }, + # dcterms:isPartOf — list of sample IDs that belong to this dataset + # (these are added as triples on each sample: sample dcterms:isPartOf dataset) + "samples": [], +} + +# Math-operation template for ASMO arithmetic activities. +# Operands may be a local property ID string (referencing an earlier +# calculated / input / output property by its 'id' field) or a numeric +# scalar. The result is a CalculatedProperty and may carry its own 'id' +# so subsequent math_operation entries can use it as an operand. +math_operation_template = { + "type": None, # Required: Subtraction | Addition | Multiplication | Division | Exponentiation + "result": { + "id": None, # optional local ID for use as operand in later math operations + "label": None, + "basename": None, + "value": None, # set if result value is known; otherwise leave None + "unit": None, + "associate_to_sample": [], + }, + # ── Subtraction ────────────────────────────────────────────────────────── + "minuend": None, # property ID or scalar + "subtrahend": None, # property ID or scalar + # ── Addition ───────────────────────────────────────────────────────────── + "addend": [], # list of property IDs and/or scalars + # ── Multiplication ─────────────────────────────────────────────────────── + "factor": [], # list of property IDs and/or scalars + # ── Division ───────────────────────────────────────────────────────────── + "dividend": None, # property ID or scalar + "divisor": None, # property ID or scalar + # ── Exponentiation ─────────────────────────────────────────────────────── + "base": None, # property ID or scalar + "exponent": None, # property ID or scalar +} + # Operation template for atomic-scale transformations # Contains all possible fields for all operation types operation_template = { diff --git a/conceptual_dictionary/vocabs.py b/conceptual_dictionary/vocabs.py new file mode 100644 index 0000000..d30e3f5 --- /dev/null +++ b/conceptual_dictionary/vocabs.py @@ -0,0 +1,145 @@ +# atomrdf/datamodels/workflow/method.py (method_map) +METHOD = frozenset( + { + "MolecularDynamics", + "MolecularStatics", + "DensityFunctionalTheory", + } +) + +# atomrdf/datamodels/workflow/algorithm.py (algorithm_map) +# parser alias: "UniaxialTension" -> "TensileTest" (io/workflow_parser.py) +ALGORITHM = frozenset( + { + "EquationOfStateFit", + "QuasiHarmonicApproximation", + "ThermodynamicIntegration", + "ANNNIModel", + "TensileTest", + "CompressionTest", + # legacy parser alias accepted by WorkflowParser + "UniaxialTension", + } +) + +# atomrdf/datamodels/workflow/dof.py (dof_map) +DEGREES_OF_FREEDOM = frozenset( + { + "AtomicPositionRelaxation", + "CellVolumeRelaxation", + "CellShapeRelaxation", + } +) + +# atomrdf/datamodels/workflow/ensemble.py (ensemble_map) +THERMODYNAMIC_ENSEMBLE = frozenset( + { + "CanonicalEnsemble", + "MicrocanonicalEnsemble", + "IsothermalIsobaricEnsemble", + "IsoenthalpicIsobaricEnsemble", + "GrandCanonicalEnsemble", + } +) + +# atomrdf/datamodels/workflow/potential.py (potential_map) +# Canonical names + all short aliases that atomRDF accepts without error. +POTENTIAL_TYPE = frozenset( + { + # canonical + "InteratomicPotential", + "EmbeddedAtomModel", + "ModifiedEmbeddedAtomModel", + "LennardJonesPotential", + "MachineLearningPotential", + # short aliases (case-sensitive as in potential_map) + "EAM", + "eam", + "eam/alloy", + "eam/fs", # LAMMPS pair_style strings, used in practice + "MEAM", + "meam", + "ACE", + "pace", + "LJ", + "lj", + "HDNNP", + "hdnnp", + "grace", + "GRACE", # GRACE machine learning potential + } +) + +# atomrdf/datamodels/workflow/xcfunctional.py (xc_map) +# Unknown strings fall back to a generic XCFunctional node (no error in atomRDF), +# but listing known values makes typos visible. +XC_FUNCTIONAL = frozenset( + { + "LDA", + "GGA", + "PBE", # alias → GGA + } +) + +# atomrdf/io/workflow_parser.py OPERATION_MAP +OPERATION_METHOD = frozenset( + { + "DeleteAtom", + "SubstituteAtom", + "AddAtom", + "Rotate", + "Rotation", # alias accepted by parser + "Translate", + "Translation", # alias accepted by parser + "Shear", + } +) + +# atomrdf/datamodels/workflow/math_operations.py MATH_OPERATION_MAP +MATH_OPERATION_TYPE = frozenset( + { + "Subtraction", + "Addition", + "Multiplication", + "Division", + "Exponentiation", + } +) + +# atomrdf/datamodels/defects/grainboundary.py +# These are the YAML key names that atomRDF maps to PLDO.* RDF types. +GRAIN_BOUNDARY_TYPE = frozenset( + { + "grain_boundary", + "tilt_grain_boundary", + "twist_grain_boundary", + "symmetric_tilt_grain_boundary", + "mixed_grain_boundary", + } +) + +# atomrdf/io/workflow_parser.py (top-level section keys read by parse()) +YAML_TOP_LEVEL_KEYS = frozenset( + { + "computational_sample", + "workflow", + "operation", + "activity", # legacy alias for "operation" + "math_operation", + } +) + +# ------------------------------------------------------------------- +# Convenience dict: field_path → (frozenset of allowed values, scope) +# "scope" is just a human-readable note for error messages. +# ------------------------------------------------------------------- +CONTROLLED_VALUES = { + "workflow.method": (METHOD, "workflow entry"), + "workflow.algorithm": (ALGORITHM, "workflow entry"), + "workflow.degrees_of_freedom": (DEGREES_OF_FREEDOM, "workflow entry (each item)"), + "workflow.thermodynamic_ensemble": (THERMODYNAMIC_ENSEMBLE, "workflow entry"), + "workflow.potential_type": (POTENTIAL_TYPE, "workflow.interatomic_potential"), + "workflow.xc_functional": (XC_FUNCTIONAL, "workflow entry"), + "operation.method": (OPERATION_METHOD, "operation entry"), + "math_operation.type": (MATH_OPERATION_TYPE, "math_operation entry"), +} diff --git a/pyproject.toml b/pyproject.toml index 57aaa51..ad606dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "conceptual-dictionary" -version = "0.1.2" +version = "0.2.0" description = "A Python dictionary template for storing serializable metadata" readme = "README.md" requires-python = ">=3.7"