{ "cells": [ { "cell_type": "markdown", "id": "f2299c7a", "metadata": {}, "source": [ "# ★★★★★ Five-Star Battery Data\n", "\n", "In this notebook, we demonstrate how to complete the Five-Star Battery Data journey by linking a structured battery dataset to **external knowledge sources**. This reflects the goal of star 5: **linked data**.\n", "\n", "We will:\n", "- Load ontology-annotated metadata from Zenodo\n", "- Parse it using RDF tools\n", "- Extract identifiers for materials used in the test object\n", "- Query Wikidata to retrieve additional information\n", "\n", "---\n", "\n", "## Watch\n", "\n", "
\n", " \n", "
\n", "\n", "---" ] }, { "cell_type": "code", "execution_count": 1, "id": "90b2ad81", "metadata": {}, "outputs": [], "source": [ "import requests\n", "from time import sleep\n", "from IPython.display import display, Image, Markdown\n", "from rdflib import Graph\n", "from rdflib.namespace import RDF\n", "from ontopy import get_ontology\n", "import pandas as pd\n" ] }, { "cell_type": "markdown", "id": "c388c3f1", "metadata": {}, "source": [ "---\n", "\n", "## Loading JSON-LD Metadata into an RDF Graph\n", "\n", "This code block performs the task of retrieving and parsing structured metadata that is encoded in JSON-LD (JavaScript Object Notation for Linked Data). The metadata file describes a battery dataset using RDF (Resource Description Framework) and vocabulary terms from battery-specific ontologies.\n", "\n", "#### Step-by-step explanation:\n", "\n", "1. The variable `metadata_url` is assigned the URL of a JSON-LD file hosted on Zenodo.\n", "2. An empty RDF graph is created using `rdflib.Graph()`. This graph is capable of storing triples in the form of subject–predicate–object.\n", "3. The `g.parse(...)` function loads and parses the JSON-LD file directly from the specified URL into the RDF graph. The `format=\"json-ld\"` argument explicitly informs the parser that the content is JSON-LD.\n", "4. After parsing, the code checks the length of the graph.\n", " - If the graph contains one or more RDF triples, it prints a success message showing the total number of triples loaded.\n", " - If the graph is empty, it prints a warning message.\n", "\n", "This RDF graph is essential for subsequent semantic queries (using SPARQL), which allow us to locate and extract information such as dataset distributions, data schemas, and semantic annotations. The RDF graph represents the structured metadata in a format that is both machine-readable and semantically meaningful.\n" ] }, { "cell_type": "code", "execution_count": 2, "id": "804ef570", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✅ Loaded 455 triples.\n" ] } ], "source": [ "metadata_url = \"https://zenodo.org/records/15553919/files/metadata.jsonld\"\n", "\n", "# Create an RDF graph\n", "g = Graph()\n", "\n", "# Parse local JSON-LD file\n", "g.parse(metadata_url, format=\"json-ld\")\n", "\n", "# Print how many triples were loaded\n", "if len(g) > 0:\n", " print(f\"✅ Loaded {len(g)} triples.\")\n", "else:\n", " print(\"⚠️ No triples were loaded from the file.\")\n", "\n" ] }, { "cell_type": "markdown", "id": "f59ba6c6", "metadata": {}, "source": [ "---\n", "\n", "## Loading the Battery Ontology into Memory and Parsing it into the RDF Graph\n", "\n", "This code block loads the battery ontology into two parallel representations: one as a Python-accessible object model using `EMMOntoPy`, and one as RDF triples into the existing `rdflib` graph.\n", "\n", "### Step-by-step explanation:\n", "\n", "1. The variable `battinfo_url` is assigned the URL of the inferred version of the Battery Ontology. This ontology is hosted at a persistent identifier managed by w3id.org and contains semantic definitions for battery-related concepts.\n", "\n", "2. The line `battinfo = get_ontology(battinfo_url).load()` uses the `EMMOntoPy` interface to load the ontology into a structured Python object model. This allows programmatic access to ontology classes, properties, and relationships using methods like `.classes()`, `.get_by_label()`, or `.search()`.\n", "\n", "3. Simultaneously, the same ontology file is parsed into the RDF graph `g` using `g.parse(...)` with the format specified as `\"turtle\"`. This ensures that the semantic content of the ontology is available for SPARQL queries in the same graph that contains the dataset metadata.\n", "\n", "4. After parsing, the graph is inspected to determine whether any triples were successfully added.\n", " - If the graph contains one or more RDF triples, it prints a success message indicating the total number of triples.\n", " - If no triples were added, it prints a warning message indicating potential failure.\n", "\n", "This dual loading approach (into both `EMMOntoPy` and `rdflib.Graph`) provides flexible access to the ontology: human-readable and programmatic via Python classes, and queryable via SPARQL in the RDF graph.\n" ] }, { "cell_type": "code", "execution_count": 3, "id": "119972cb", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✅ Loaded 52269 triples.\n" ] } ], "source": [ "battinfo_url = \"https://w3id.org/emmo/domain/battery/inferred\"\n", "\n", "# Loading from web\n", "battinfo = get_ontology(battinfo_url).load()\n", "g.parse(battinfo_url, format=\"turtle\")\n", "\n", "# Print how many triples were loaded\n", "if len(g) > 0:\n", " print(f\"✅ Loaded {len(g)} triples.\")\n", "else:\n", " print(\"⚠️ No triples were loaded from the file.\")" ] }, { "cell_type": "markdown", "id": "0e11cae6", "metadata": {}, "source": [ "---\n", "\n", "## Querying the RDF Graph for Active Materials and Their Semantic Annotations\n", "\n", "This code block defines and executes a SPARQL query that retrieves information about the active materials present in a battery dataset, as described by the RDF metadata and associated ontologies.\n", "\n", "### Step-by-step explanation:\n", "\n", "1. A multi-line SPARQL query is defined using a Python f-string, allowing dynamic insertion of property IRIs from the `battinfo` ontology object. The query performs the following:\n", "\n", " - It selects three variables: `?material`, `?type`, and `?wikidata`.\n", " - It matches any triple in the RDF graph where a subject `?cell` has a `hasActiveMaterial` relationship to an object `?material`. The `hasActiveMaterial` property IRI is injected from the ontology model using `battinfo.hasActiveMaterial.iri`.\n", " - It optionally retrieves the RDF type (`rdf:type`) of each material using `OPTIONAL { ?material rdf:type ?type }`. This ensures the query does not fail if the type is missing.\n", " - It optionally retrieves a `wikidataReference` IRI associated with the material’s type using `OPTIONAL { ?type battinfo:wikidataReference ?wikidata }`.\n", "\n", "2. The SPARQL query is executed using `g.query(query)`, where `g` is the RDF graph containing both the dataset metadata and the battery ontology.\n", "\n", "3. The results are iterated over and printed to the console. For each row:\n", " - The material IRI is printed.\n", " - If an RDF type is available, it is shown; otherwise, a placeholder string `\"(no rdf:type)\"` is used.\n", " - If a Wikidata reference is available, it is shown; otherwise, `\"(no Wikidata ID)\"` is printed.\n", "\n", "This step allows for tracing each active material in the dataset to its corresponding class and external identifier (e.g. Wikidata), enabling interoperability with global knowledge graphs and material databases.\n" ] }, { "cell_type": "code", "execution_count": 4, "id": "b4ca6158", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "🔍 Materials assigned via hasActiveMaterial, their types, and Wikidata references:\n", "- https://zenodo.org/records/15553919#81867a7d-25e2-437c-8aff-104dc6aa9c45 (type: https://w3id.org/emmo/domain/chemical-substance#substance_4c62d334_a124_40b3_9fd1_fe713d01a6af, Wikidata: https://www.wikidata.org/wiki/Q415891)\n", "- https://zenodo.org/records/15553919#68ff065e-bf7e-47fa-abde-42b82e8e2d54 (type: https://w3id.org/emmo/domain/chemical-substance#substance_d53259a7_0d9c_48b9_a6c1_4418169df303, Wikidata: https://www.wikidata.org/wiki/Q5309)\n" ] } ], "source": [ "# Define and run the SPARQL query using the resolved IRI\n", "query = f\"\"\"\n", "SELECT DISTINCT ?material ?type ?wikidata\n", "WHERE {{\n", " ?cell <{battinfo.hasActiveMaterial.iri}> ?material .\n", " OPTIONAL {{ ?material <{RDF.type}> ?type }}\n", " OPTIONAL {{ ?type <{battinfo.wikidataReference.iri}> ?wikidata }}\n", "}}\n", "\"\"\"\n", "\n", "results = g.query(query)\n", "\n", "# Display results\n", "print(\"🔍 Materials assigned via hasActiveMaterial, their types, and Wikidata references:\")\n", "for row in results:\n", " material = row.material\n", " typ = row.type if row.type else \"(no rdf:type)\"\n", " qid = row.wikidata if row.wikidata else \"(no Wikidata ID)\"\n", " print(f\"- {material} (type: {typ}, Wikidata: {qid})\")\n" ] }, { "cell_type": "markdown", "id": "ad431aec", "metadata": {}, "source": [ "---\n", "\n", "## Querying Wikidata for Material Properties and Images\n", "\n", "This code block queries the Wikidata SPARQL endpoint to retrieve additional semantic information about materials used in a battery dataset. Specifically, it attempts to extract the following for each material with a known Wikidata reference:\n", "\n", "- The human-readable label (English name)\n", "- The density of the material (`wdt:P2054`)\n", "- A structural or schematic image (`wdt:P8224`)\n", "- A general photographic image (`wdt:P18`)\n", "\n", "### Step-by-step explanation:\n", "\n", "1. The SPARQL endpoint for Wikidata is defined as `https://query.wikidata.org/sparql`.\n", "\n", "2. A message is printed to indicate the start of the Wikidata lookup process.\n", "\n", "3. The loop iterates over the `results` obtained from a prior SPARQL query against the local RDF graph, where each `row` may contain a `wikidata` URI identifying a material class.\n", "\n", "4. For each `row`:\n", " - It checks if a `wikidata_uri` is present. If it is not, the row is skipped.\n", " - The last segment of the URI (e.g., `Q42512`) is extracted and stored as `wikidata_id`.\n", "\n", "5. A SPARQL query is constructed to fetch:\n", " - The English label of the Wikidata entity using `rdfs:label` filtered by `lang=\"en\"`.\n", " - The density property (`wdt:P2054`) if it exists.\n", " - A structural or schematic image (`wdt:P8224`) if available.\n", " - A photographic image (`wdt:P18`) if available.\n", "\n", "6. The query is sent to the Wikidata endpoint using an HTTP GET request, with the response format specified as JSON.\n", "\n", "7. If the HTTP response is successful (`status_code == 200`):\n", " - The JSON results are parsed.\n", " - If at least one result is returned:\n", " - The material's label, density, and image URLs (if present) are extracted from the result.\n", " - The label and density are printed.\n", " - If image URLs exist:\n", " - The images are displayed directly in the notebook using `IPython.display.Image`, each scaled to 300 pixels width.\n", " - If no images are available, a message indicates this.\n", " - If no data is returned for the material, a message is printed indicating the failure.\n", "\n", "8. If the response is not successful, an error message is printed with the HTTP status code.\n", "\n", "9. A `sleep(1)` call is used to pause for one second between requests. This is done to avoid sending queries too rapidly and overwhelming the public SPARQL endpoint, which could lead to throttling or blocking.\n", "\n", "This code enriches local dataset metadata by linking it to publicly maintained knowledge in Wikidata, making the dataset more informative and connected within the global web of data.\n" ] }, { "cell_type": "code", "execution_count": 5, "id": "4dd00cdc", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "🔍 Querying Wikidata for material density (P2054) and images (P8224, P18):\n", "\n", "🧪 lithium cobalt oxide\n", " - Density: 1.68 g/cm³\n", " - Structure image (P8224):\n" ] }, { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "\n", "🧪 graphite\n", " - Density: 2.16 g/cm³\n", " - Photo image (P18):\n" ] }, { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# Define the SPARQL endpoint for Wikidata\n", "wikidata_endpoint = \"https://query.wikidata.org/sparql\"\n", "\n", "print(\"🔍 Querying Wikidata for material density (P2054) and images (P8224, P18):\")\n", "\n", "# Loop through each row of SPARQL query results from your RDF graph\n", "for row in results:\n", " wikidata_uri = row[\"wikidata\"]\n", " \n", " # Continue only if the material has a valid Wikidata URI\n", " if wikidata_uri:\n", " # Extract the QID (e.g., \"Q42512\") from the full Wikidata URL\n", " wikidata_id = str(wikidata_uri).split('/')[-1]\n", "\n", " # Define a SPARQL query to fetch:\n", " # - the English label (human-readable name)\n", " # - the density (P2054)\n", " # - a structure image (P8224)\n", " # - a general photo image (P18)\n", " query = f\"\"\"\n", " SELECT ?label ?density ?img1 ?img2 WHERE {{\n", " wd:{wikidata_id} rdfs:label ?label .\n", " FILTER (lang(?label) = \"en\")\n", "\n", " OPTIONAL {{ wd:{wikidata_id} wdt:P2054 ?density . }}\n", " OPTIONAL {{ wd:{wikidata_id} wdt:P8224 ?img1 . }}\n", " OPTIONAL {{ wd:{wikidata_id} wdt:P18 ?img2 . }}\n", " }}\n", " \"\"\"\n", "\n", " # Send the query to the Wikidata SPARQL endpoint\n", " response = requests.get(wikidata_endpoint, params={'query': query, 'format': 'json'})\n", "\n", " if response.status_code == 200:\n", " # Parse the JSON response from Wikidata\n", " bindings = response.json().get('results', {}).get('bindings', [])\n", "\n", " # If results are found for the material\n", " if bindings:\n", " b = bindings[0] # Get the first result row\n", " label = b['label']['value'] # Material label (e.g., \"Graphite\")\n", " density = b.get('density', {}).get('value', 'N/A') # Density value if present\n", " img1 = b.get('img1', {}).get('value', None) # Structure image URL\n", " img2 = b.get('img2', {}).get('value', None) # Photo image URL\n", "\n", " # Print the label and density\n", " print(f\"\\n🧪 {label}\")\n", " print(f\" - Density: {density} g/cm³\" if density != 'N/A' else \" - Density: ❌ not available\")\n", "\n", " # Display structure image (chemical diagram or schematic)\n", " if img1:\n", " print(f\" - Structure image (P8224):\")\n", " display(Image(url=img1, width=300)) # Scaled to 300 px width\n", "\n", " # Display photo image (e.g., macro photograph of the substance)\n", " if img2:\n", " print(f\" - Photo image (P18):\")\n", " display(Image(url=img2, width=300)) # Scaled to 300 px width\n", "\n", " # If neither image is available\n", " if not img1 and not img2:\n", " print(\" - Images: ❌ none available\")\n", " else:\n", " print(f\"- {wikidata_id}: ❌ no data returned from SPARQL query\")\n", " else:\n", " print(f\"- {wikidata_id}: ❌ SPARQL query failed (HTTP {response.status_code})\")\n", "\n", " # Pause between requests to avoid hitting the endpoint too frequently\n", " sleep(1)\n" ] }, { "cell_type": "markdown", "id": "7a63441d", "metadata": {}, "source": [ "## Summary\n", "\n", "In this notebook, you learned how to satisfy the requirements for **5-star battery data** by linking your dataset to external data sources like **Wikidata** and **PubChem**.\n", "\n", "| Step | What You Did |\n", "|-------------------|------------------------------------------------------------|\n", "| Load metadata | Parsed JSON-LD/RDFa metadata from a published Zenodo record|\n", "| Extract materials | Identified semantic IRIs for active materials |\n", "| Link externally | Queried Wikidata using SPARQL to retrieve additional info |\n", "\n", "This allows your dataset to become part of a larger **knowledge graph**, enabling automated reasoning, richer search, and future integration with AI agents.\n" ] }, { "cell_type": "markdown", "id": "e9f49a6a", "metadata": {}, "source": [ "---\n", "\n", "\"EU\n", "\n", "**This work has received funding from the European Union under the Horizon Europe programme.** \n", "Views and opinions expressed are however those of the author(s) only and do not necessarily reflect those of the European Union or the European Commission. Neither the European Union nor the granting authority can be held responsible for them." ] } ], "metadata": { "kernelspec": { "display_name": ".venv", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.10" } }, "nbformat": 4, "nbformat_minor": 5 }