diff --git a/docs/api_reference/strategies/add_instance.md b/docs/api_reference/strategies/add_instance.md new file mode 100644 index 00000000..9a73f314 --- /dev/null +++ b/docs/api_reference/strategies/add_instance.md @@ -0,0 +1,3 @@ +# add_instance + +::: oteapi_dlite.strategies.add_instance diff --git a/oteapi_dlite/strategies/add_instance.py b/oteapi_dlite/strategies/add_instance.py new file mode 100644 index 00000000..ab8901a3 --- /dev/null +++ b/oteapi_dlite/strategies/add_instance.py @@ -0,0 +1,98 @@ +"""Generic function strategy using DLite storage plugin.""" +# pylint: disable=unused-argument,invalid-name +from typing import TYPE_CHECKING, Optional + +import dlite +from dlite.utils import infer_dimensions +from oteapi.models import AttrDict, FunctionConfig, SessionUpdate +from pydantic import Field +from pydantic.dataclasses import dataclass + +from oteapi_dlite.models import DLiteSessionUpdate +from oteapi_dlite.utils import get_collection, update_collection + +if TYPE_CHECKING: + from typing import Any, Dict + + from oteapi.interfaces import IFunctionStrategy + + +class AddInstanceConfig(AttrDict): + """Configuration for adding an instance to the collection.""" + + datamodel: str = Field( + description="ID (URI or UUID) of the datamodel.", + ) + property_values: dict = Field( + description="Dict with property values.", + ) + dimensions: "Optional[dict]" = Field( + None, + description="Dict with dimension values. If not provided, the " + "dimensions will be inferred from `values`.", + ) + label: str = Field( + ..., + description="Label of DLite instance to serialise in the collection.", + ) + + +class DLiteAddInstanceConfig(FunctionConfig): + """DLite function strategy config.""" + + configuration: AddInstanceConfig = Field( + ..., + description="Strategy-specific configuration for adding " + "an instance to the collection.", + ) + + +@dataclass +class DLiteAddInstanceStrategy: + """DLite function strategy to add an Instance to the collection. + + **Registers strategies**: + + - `("mediaType", "application/vnd.dlite-addinstance")` + + """ + + function_config: DLiteAddInstanceConfig + + def initialize( + self, + session: "Optional[Dict[str, Any]]" = None, + ) -> "SessionUpdate": + """Initialize.""" + return SessionUpdate() + + def get( + self, session: "Optional[Dict[str, Any]]" = None + ) -> "DLiteSessionUpdate": + """Execute the strategy. + + This method will be called through the strategy-specific endpoint + of the OTE-API Services. + + Parameters: + session: A session-specific dictionary context. + + Returns: + SessionUpdate instance. + """ + config = self.function_config.configuration + + coll = get_collection(session) + datamodel = dlite.get_instance(config.datamodel) + if config.dimensions is None: + dims = infer_dimensions( + datamodel, config.property_values, strict=True + ) + else: + dims = config.dimensions + inst = datamodel(dimensions=dims, properties=config.values) + + coll.add(label=config.label, inst=inst) + + update_collection(coll) + return DLiteSessionUpdate(collection_id=coll.uuid) diff --git a/oteapi_dlite/strategies/function.py b/oteapi_dlite/strategies/function.py index 6b54e0ce..736f8754 100644 --- a/oteapi_dlite/strategies/function.py +++ b/oteapi_dlite/strategies/function.py @@ -115,6 +115,7 @@ def get( ) coll = get_collection(session, config.collection_id) + inst = coll[config.label] # Save instance diff --git a/requirements_dev.txt b/requirements_dev.txt index 2971bd1d..9974417c 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,3 +1,4 @@ +otelib~=0.3.0 pre-commit~=3.3 pylint~=2.17 pytest~=7.4 diff --git a/setup.cfg b/setup.cfg index aff12889..f3e19d03 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,9 +29,10 @@ oteapi.parse = oteapi_dlite.image/vnd.dlite-png = oteapi_dlite.strategies.parse_image:DLiteImageParseStrategy oteapi_dlite.image/vnd.dlite-tiff = oteapi_dlite.strategies.parse_image:DLiteImageParseStrategy - oteapi.function = - oteapi_dlite.application/vnd.dlite-generate = oteapi_dlite.strategies.generate:DLiteGenerateStrategy + oteapi_dlite.application/vnd.dlite-generate = oteapi_dlite.strategies.generate:DLiteFunctionStrategy + oteapi_dlite.application/vnd.dlite-function = oteapi_dlite.strategies.function:DLiteFunctionStrategy + oteapi_dlite.application/vnd.dlite-addinstance = oteapi_dlite.strategies.add_instance:DLiteAddInstanceStrategy oteapi.mapping = oteapi_dlite.mappings = oteapi_dlite.strategies.mapping:DLiteMappingStrategy diff --git a/tests/output/test_add_instance.json b/tests/output/test_add_instance.json new file mode 100644 index 00000000..44388dbe --- /dev/null +++ b/tests/output/test_add_instance.json @@ -0,0 +1,13 @@ +{ + "7bad2efc-cb40-420b-aef9-2f909d038b46": { + "meta": "http://onto-ns.com/meta/0.1/Result", + "dimensions": { + "natoms": 2, + "ncoords": 3 + }, + "properties": { + "potential_energy": 0, + "forces": [[0, 0, 0], [0, 0, 0]] + } + } +} diff --git a/tests/strategies/test_add_instance.py b/tests/strategies/test_add_instance.py new file mode 100644 index 00000000..523d56f9 --- /dev/null +++ b/tests/strategies/test_add_instance.py @@ -0,0 +1,59 @@ +"""Test script for the add_instance strategy - tested via otelib.""" +import os +from pathlib import Path + +import dlite +import numpy as np +from otelib import OTEClient + +# Paths +thisdir = Path(__file__).resolve().parent +testdir = thisdir.parent +entitydir = testdir / "entities" +outdir = testdir / "output" + +os.makedirs(outdir, exist_ok=True) +dlite.storage_path.append(entitydir) + + +values = { + "potential_energy": 3.2e-19, + "forces": [ + [1.2, 2.3, 3.4], + [0.2, 3.4, 4.5], + ], +} + +# Create OTE client +client = OTEClient("python") + +add_instance = client.create_function( + functionType="application/vnd.dlite-addinstance", + configuration={ + "datamodel": "http://onto-ns.com/meta/0.1/Result", + "property_values": values, + "label": "result", + }, +) + + +generate = client.create_function( + functionType="application/vnd.dlite-generate", + configuration={ + "driver": "json", + "location": f"{outdir}/test_add_instance.json", + "options": "mode=w", + "label": "result", + }, +) + +pipeline = add_instance >> generate +pipeline.get() + + +# Test that the created content is as expected +inst = dlite.Instance.from_location( + driver="json", location=f"{outdir}/test_add_instance.json", options="mode=r" +) +assert np.allclose(inst.potential_energy, 3.2e-19) +assert np.allclose(inst.forses, [[1.2, 2.3, 3.4], [0.2, 3.4, 4.5]])