This repository provides a proof of concept for transforming a knowledge graph (KG) expressed in RDF (Resource Description Framework) into an Asset Administration Shell (AAS). The KG can be modeled according to ontologies such as PMDCo (Platform Material Digital Core Ontology), EMMO (Elementary Multiperspective Materials Ontology), or other relevant frameworks.
This repository is not intended as a fully modular or packaged software solution. Instead, it serves as a guideline for leveraging semantic technologies, such as SPARQL and JSON-LD, to map and transform data models between top-level ontologies (TLOs) and the AAS framework.
As input we expecting the following implementations:
- The Knowledge graph (KG) expressed in OWL - this input is different for each pipeline configuration
- A SPARQL Construct Query - this input is different for each pipeline configuration
- A JSON LD Context Mapping - this is reusable for each pipeline scenario
- Some minimal manual wrangling using REGEX, executed in a suitable programming language (e.g. Python) - this is reusable for each pipeline scenario
- The AAS Metamodel JSON Schema - this is reusable for each pipeline scenario
Expected output:
- A validated AAS instance (serialized in JSON) with metadata from the input KG
- Any input knowledge graph in OWL as well as any output AAS has a dedicated structure and pattern. This means that any SPARQL Construct needs to be specifically developed according to both.
- Currently, the provided
@contextfrom JSON-LD is not properly framing all JSON-property-keys of objects likeProperty,ReferableandQualifierlikeaas:Property/value,aas:Qualifier/valueaccording to the official AAS Metamodel JSON Schema. This is why we need some manual wrangling of the JSON using REGEX and other filtering. This is explain in more detail in section 1.2.2 - The
SemanticIDfrom AAS generally expects a single class reference for eachProperty, such as e.g.MeanTensileStrengthfrom the Inspection Documents of Steel Products. This might be a generall mismatch when mapping to the input KG in OWL, since Top-level ontologies (TLOs) generally do not define extra classes for the context (e.g. mean, min, max, standard deviation, etc.) of each physical property such asTensileStrengthbut rather describe it by the usage of multiple class instances and respective object properties (e.g.pmd:Material-hasMeanProperty->pmd:TensileStrength-qudt:value->784.094^^xsd:float)
In the following chapter, we will explain step by step the proposed pipeline. The full script written in Python can be found under examples/demo.py
The SPARQL query generally needs to have two parts:
- The WHERE-part, which is traversing nodes of the input KG in OWL and fetching the metadata to be reflected in the AAS (Lower part of the query)
- The CONSTRUCT-part, which is setting up the AAS datamodel according to the official AAS metamodel ontology and the corresponding AAS template of interest (Upper part of the query)
CONSTRUCT {
<The AAS model according to the template goes here>
}
WHERE {
<The query for fetching metadata from the KG goes here>
}The CONSTRUCT-part can be easily derived by transforming an .aasx-reference instance of the targeted AAS-template by using the basyx-python-sdk (Experimental/Adapter/RDF-sidebranch):
import os
from basyx.aas import model
from basyx.aas.adapter.aasx import AASXReader, DictSupplementaryFileContainer
from basyx.aas.adapter.rdf import write_aas_rdf_file
from basyx.aas.adapter.json import write_aas_json_file
cwd = os.path.dirname(__file__)
aasx_path = os.path.join(cwd, "aas", "model.aasx")
json_path = os.path.join(cwd, "aas", "model.json")
ttl_path = os.path.join(cwd, "aas", "model.ttl")
objects = model.DictObjectStore()
files = DictSupplementaryFileContainer()
with AASXReader(aasx_path) as reader:
metadata = reader.get_core_properties()
reader.read_into(objects, files)
write_aas_rdf_file(ttl_path, objects)
write_aas_json_file(json_path, objects, indent=4)The full model.json, model.aasx, model.ttl can be found under examples/aas/model.json, examples/aas/model.aasx and examples/aas/model.ttl. The executing script is available under examples/extract_aas_model.py.
The model.ttl-output from this script then contains the RDF-model of the AAS to be targeted by the SPARQL Constuct Query.
See the following snippet as example:
[
a aas:Property ;
ns6:semanticId [ a aas:Reference ;
ns1:keys [ a aas:Key ;
ns4:type <https://admin-shell.io/aas/3/0/KeyTypes/GlobalReference> ;
ns4:value "https://admin-shell.io/idta/InspectionDocumentsOfSteelProducts/TensileStrengthMean/1/0"^^xsd:string ] ;
ns1:type <https://admin-shell.io/aas/3/0/ReferenceTypes/ExternalReference> ] ;
ns13:valueType <https://admin-shell.io/aas/3/0/DataTypeDefXsd/xs:float> ;
ns14:qualifiers [ a aas:Qualifier ;
ns6:semanticId [ a aas:Reference ;
ns1:keys [ a aas:Key ;
ns4:type <https://admin-shell.io/aas/3/0/KeyTypes/GlobalReference> ;
ns4:value "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"^^xsd:string ] ;
ns1:type <https://admin-shell.io/aas/3/0/ReferenceTypes/ExternalReference> ] ;
ns8:kind <https://admin-shell.io/aas/3/0/QualifierKind/TemplateQualifier> ;
ns8:type "SMT/Cardinality"^^xsd:string ;
ns8:value "ZeroToOne"^^xsd:string ;
ns8:valueType <https://admin-shell.io/aas/3/0/DataTypeDefXsd/xs:string> ] ;
ns5:displayName [ a aas:LangStringNameType ;
ns3:language "en"^^xsd:string ;
ns3:text "Tensile Strength Mean"^^xsd:string ],
[ a aas:LangStringNameType ;
ns3:language "de"^^xsd:string ;
ns3:text "Zugfestigkeit Mittelwert"^^xsd:string ] ;
ns5:idShort "TensileStrengthMean"^^xsd:string ,
aas:Property/value "784.094"^xsd:float
] .Please note the very last line with aas:Property/value "784.094"^xsd:float, which contains a hardcoded value ("784.094"^xsd:float) to be manually replaced with a variable (e.g. ?tensile_strength) corresponding to a variable with the same name from the WHERE-part.
After inserting this model with the variables into the CONSTRUCT-part, the WHERE-part needs to be defined.
Let's consider a very simple knowledge graph with a couple of entities:
fileid:ModulusOfElasticity a <https://w3id.org/steel/ProcessOntology/ModulusOfElasticity> ;
qudt:hasUnit "http://qudt.org/vocab/unit/MegaPA"^^xsd:anyURI ;
qudt:value "208997.842"^^xsd:float .
fileid:PoissonRatio a <https://w3id.org/steel/ProcessOntology/PoissonRatio> ;
qudt:hasUnit "http://qudt.org/vocab/unit/NUM"^^xsd:anyURI ;
qudt:value "0.3"^^xsd:float .
fileid:ProofStrength_0.2Percentage a <https://w3id.org/steel/ProcessOntology/ProofStrength_0.2Percentage> ;
qudt:hasUnit "http://qudt.org/vocab/unit/MegaPA"^^xsd:anyURI ;
qudt:value "509.255"^^xsd:float .
fileid:TensileStrength a <https://w3id.org/steel/ProcessOntology/TensileStrength> ;
qudt:hasUnit "http://qudt.org/vocab/unit/MegaPA"^^xsd:anyURI ;
qudt:value "782.094"^^xsd:float .
The full file can be found under examples/input/test.ttl.
These mechanical properties can be semantically described within a characterization process according to a suitable ontology, such as PMDCo.
In order to extract the needed ?tensile_strength for the AAS from this KG, we can simply write the following WHERE-part for our SPARQL Query:
fileid:TensileStrength a <https://w3id.org/steel/ProcessOntology/TensileStrength> ;
qudt:value ?tensile_strength .The full mapping expressed by the SPARQL Construct can be found under examples/input/mapping.sparql.
By using any SPARQL Engine in reference implementations like RDFLib, pyoxigraph, etc. and the SPARQL Construct defined in the previous section, we can transform the KG model into the AAS model we have defined in the query.
import json
from rdflib import Graph
g = rdflib.Graph()
g.parse(<your-input-kg>)
result = g.query(<your-sparql-construct>).serialize(format="json-ld")
json_ld = json.loads(result)The output of this step is an AAS serialized in JSON-LD.
The JSON LD context is used for framing the resulting AAS in JSON LD from the previous step into the official AAS JSON Schema according.
An extraction of an example context may look like this:
{
"@context": {
"aas": "https://admin-shell.io/aas/3/0/",
"xs": "http://www.w3.org/2001/XMLSchema#",
"aas:value": {
"@type": "xs:float"
},
"idShort": "aas:Referable/idShort",
"modelType": {
"@id": "@type",
"@type": "@vocab"
},
"id": "aas:Identifiable/id",
"kind": {
"@id": "aas:HasKind/kind",
"@type": "@vocab"
},
"qualifiers": {
"@id": "aas:Qualifiable/qualifiers",
"@container": "@set",
"@context": {
"semanticId": {
"@id": "aas:HasSemantics/semanticId",
"@context": {
"type": {
"@id": "aas:Reference/type",
"@type": "@vocab"
},
"keys": {
...
}
}
}
}
}
...
}The full @context can be found under examples/input/frame_slim.json, which was originally derived from the officially released AAS JSON LD Contexts from GitHub.
We are working here with global and local @contexts in the frame, since the official AAS Json Schema is expecting keys like value, type or valueType in multiple objects like Property, Referable and Qualifier, which are expressed through different IRIs in the different contexts, such as aas:Property/value, aas:Property/valueType, aas:Qualifier/valueType, aas:Qualifier/value, etc.
The framing can be executed by using the pyld library in the following code snippet:
from pyld import jsonld
json_ld = jsonld.frame(json_ld, frame)
output = jsonld.compact(json_ld, frame["@context"])Depending on how well the @context is defined, the framing still might not lead to a JSON body, which is 100%-ly following the AAS JSON Schema.
The current frame we have provided in this repository has issues with resolving the IRIs properly in the global and the local context hence, there still will be keys like aas:Property/value in the framed JSON document, which are not compliant to the AAS JSON Schema.
This leads to the current need to perfrom small operations like string replacement using REGEX and array filtering.
At the moment, it is solved like this:
import re
import json
# replace strings according to regex patterns
patterns = [ r'"aas:[^/"]+/([^/"]+)"', r'"aas:([^/"]+)"' ]
result = json.dumps(output,indent=2)
for pattern in patterns:
result = re.sub(pattern["regex"], r'"\1"', result)
output = json.loads(result)
# get submodel node
for node in output["@graph"]:
node_type = node.get("@type")
if node_type == "Submodel":
output = node
breakThe regex is thereby replacing remanent keys like aas:Property/value or aas:value with value. The second step simply filters the JSON Body in order to return the Submodel of interest.
In order to make sure that the JSON body after framing is compliant to the AAS metamodel schema, we are applying a schema validation of our document against the official AAS JSON Schema from GitHub. We can do this by using the JSON-Schema library in Python:
import requests
from jsonschema import validate
aas_json_schema = "https://raw.githubusercontent.com/admin-shell-io/aas-specs/refs/heads/master/schemas/json/aas.json"
validate(output, requests.get(aas_json_schema).json())If no error if thrown, the JSON document should be compliant towards the standard. However, the validation is expecting that the top-level object is an AssetAdministrationShell, not any other type like Submodel, SubmodelElementCollection, etc. since they MUST be embedded into the AssetAdministrationShell-Object. Please note that there will be artifact objects and keys like @id and @type from the JSON-LD. However, since JSON Schema allows extra properties to be defined, this usually is not an issue during validation as long as all required properties of the defined objects are provided.
An example output of the workflow can be found under examples/output/output.json.
The requirements can be installed simply by pip install -r requirements.txt.
The list of packages is the following:
rdflib>=7,<8
PyLD>=2,<2.1
jsonschema>=4.25.0,<5
requests
git+https://github.com/eclipse-basyx/basyx-python-sdk.git@6bd05e54fbe6c78b30dd0926a37f8741a64d42ca
This project is licensed under the BSD 3-Clause. See the LICENSE file for more information.
Copyright (c) 2014-2024, Fraunhofer-Gesellschaft zur Förderung der angewandten Forschung e.V. acting on behalf of its Fraunhofer IWM.
Contact: Matthias Büschelberger
