Example: Zinc Powder from a Supplier#

Let’s describe an instance of some zinc powder with a set of properties defined in the specification sheet from the manufacturer!

This example covers a few topics:

  • How to describe a resource using ontology terms and JSON-LD

  • How machines convert JSON-LD into triples

  • What is the meaning of the subject, predicate, and object identifiers

  • How to run a simple query using SPARQL [Moderate]

  • How to use the ontology to fetch more information from other sources [Advanced]

A live version of this notebook is available on Google Colab here

Describe the powder using ontology terms in JSON-LD format#

The JSON-LD data that we will use is:

[104]:
jsonld = {
  "@context": "https://raw.githubusercontent.com/emmo-repo/domain-electrochemistry/master/context.json",
  "@type": ["Zinc", "Powder"],
  "schema:manufacturer": {
      "@id": "https://www.wikidata.org/wiki/Q680841",
      "schema:name": "Sigma-Aldrich"
  },
  "schema:productID": "324930",
  "schema:url": "https://www.sigmaaldrich.com/NO/en/product/aldrich/324930",
  "hasProperty": [
      {
        "@type": ["D95ParticleSize", "ConventionalProperty"],
        "hasNumericalPart": {
              "@type": "Real",
              "hasNumericalValue": 150
        },
        "hasMeasurementUnit": "emmo:MicroMetre",
        "dc:source": "https://www.sigmaaldrich.com/NO/en/product/aldrich/324930"
      }
  ]
}

Parse this description into a graph#

Now let’s see how a machine would process this data by reading it into a Graph!

First, we install and import the python dependencies that we need for this example.

[105]:
# Install and import dependencies
!pip install jsonschema rdflib requests matplotlib > /dev/null

import json
import rdflib
import requests
import sys
from IPython.display import Image, display
import matplotlib.pyplot as plt

We create the graph using a very handy python package called rdflib, which provides us a way to parse our json-ld data, run some queries using the language SPARQL, and serialize the graph in any RDF compatible format (e.g. JSON-LD, Turtle, etc.).

[106]:
# Create a new graph
g = rdflib.Graph()

# Parse our json-ld data into the graph
g.parse(data=json.dumps(jsonld), format="json-ld")

# Create a SPARQL query to return all the triples in the graph
query_all = """
SELECT ?subject ?predicate ?object
WHERE {
  ?subject ?predicate ?object
}
"""

# Execute the SPARQL query
all_the_things = g.query(query_all)

# Print the results
for row in all_the_things:
    print(row)

(rdflib.term.BNode('Nba3653d5211a479faa84120717afec04'), rdflib.term.URIRef('https://schema.org/manufacturer'), rdflib.term.URIRef('https://www.wikidata.org/wiki/Q680841'))
(rdflib.term.BNode('Nbad48dcf37014f5989126dde499c31e7'), rdflib.term.URIRef('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'), rdflib.term.URIRef('http://emmo.info/emmo#EMMO_d8aa8e1f_b650_416d_88a0_5118de945456'))
(rdflib.term.BNode('Nba17008e342643b8848eb653b0cc6c5f'), rdflib.term.URIRef('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'), rdflib.term.URIRef('http://emmo.info/emmo#EMMO_18d180e4_5e3e_42f7_820c_e08951223486'))
(rdflib.term.URIRef('https://www.wikidata.org/wiki/Q680841'), rdflib.term.URIRef('https://schema.org/name'), rdflib.term.Literal('Sigma-Aldrich'))
(rdflib.term.BNode('Nbad48dcf37014f5989126dde499c31e7'), rdflib.term.URIRef('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'), rdflib.term.URIRef('https://w3id.org/emmo/domain/electrochemistry#electrochemistry_02d2d1d1_241c_429b_b4e7_31f2c3dc4835'))
(rdflib.term.BNode('Nbad48dcf37014f5989126dde499c31e7'), rdflib.term.URIRef('http://emmo.info/emmo#EMMO_bed1d005_b04e_4a90_94cf_02bc678a8569'), rdflib.term.URIRef('http://emmo.info/emmo#MicroMetre'))
(rdflib.term.BNode('Nba3653d5211a479faa84120717afec04'), rdflib.term.URIRef('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'), rdflib.term.URIRef('https://w3id.org/emmo/domain/electrochemistry#electrochemistry_ee479886_6805_4018_95e1_500185e44215'))
(rdflib.term.BNode('Nba3653d5211a479faa84120717afec04'), rdflib.term.URIRef('https://schema.org/url'), rdflib.term.Literal('https://www.sigmaaldrich.com/NO/en/product/aldrich/324930'))
(rdflib.term.BNode('Nbad48dcf37014f5989126dde499c31e7'), rdflib.term.URIRef('http://emmo.info/emmo#EMMO_8ef3cd6d_ae58_4a8d_9fc0_ad8f49015cd0'), rdflib.term.BNode('Nba17008e342643b8848eb653b0cc6c5f'))
(rdflib.term.BNode('Nba3653d5211a479faa84120717afec04'), rdflib.term.URIRef('https://schema.org/productID'), rdflib.term.Literal('324930'))
(rdflib.term.BNode('Nba3653d5211a479faa84120717afec04'), rdflib.term.URIRef('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'), rdflib.term.URIRef('https://w3id.org/emmo/domain/chemicalsubstance#substance_9bd78e1c_a4dc_41b6_8013_adb51df1ffdc'))
(rdflib.term.BNode('Nba17008e342643b8848eb653b0cc6c5f'), rdflib.term.URIRef('http://emmo.info/emmo#EMMO_faf79f53_749d_40b2_807c_d34244c192f4'), rdflib.term.Literal('150', datatype=rdflib.term.URIRef('http://www.w3.org/2001/XMLSchema#integer')))
(rdflib.term.BNode('Nbad48dcf37014f5989126dde499c31e7'), rdflib.term.URIRef('http://purl.org/dc/elements/1.1/source'), rdflib.term.Literal('https://www.sigmaaldrich.com/NO/en/product/aldrich/324930'))
(rdflib.term.BNode('Nba3653d5211a479faa84120717afec04'), rdflib.term.URIRef('http://emmo.info/emmo#EMMO_e1097637_70d2_4895_973f_2396f04fa204'), rdflib.term.BNode('Nbad48dcf37014f5989126dde499c31e7'))

You can see that our human-readable JSON-LD file has been transformed into some nasty looking (but machine-readable!) triples. Let’s look at a couple in more detail to understand what’s going on.

Examine and explore the triples#

Let’s start with this one:

subject

https://www.wikidata.org/wiki/Q680841

predicate

https://schema.org/name

object

‘Sigma-Aldrich’

This tells the machine that something with a wikidata identifier has a property called ‘name’ from the schema.org vocabulary with a literal value ‘Sigma-Aldrich’. These identifiers serve not only as persistent and unique identifiers for the concepts, but also point to a place where a machine can go to learn more about what it is. Try it yourself! Click on one and see where it takes you!

Neat, right?! Let’s look at another one:

subject

‘Nb9d4bdc220954548a09b8b56f95d9cf3’

predicate

http://www.w3.org/1999/02/22-rdf-syntax-ns#type

object

https://w3id.org/emmo/domain/chemicalsubstance#substance_9bd78e1c_a4dc_41b6_8013_adb51df1ffdc

This tells the machine that a certain node in the graph is a a type of some thing that exists in the EMMO domain ‘chemicalsubstance’. And this gets to one of the difficult bits for humans: many ontologies (like EMMO) use UUIDs for term names to ensure that they are universally unique. It works, but it sacrifices the human readability. Luckily we can get around this by assigning human-readable annotations to that term and/or mapping the IRI to a human readable label in a JSON-LD context like we did above.

Go ahead, click the link and see if you can figure out what this thing is…

it’s Zinc! Now we can see how our simple description in the JSON-LD file has now been converted to a machine-readable IRI.

Query the graph using SPARQL [Moderate]#

Now, let’s write a SPARQL query to get back some specific thing…like what is the name of the manufacturer?

[107]:
query = """
PREFIX schema: <https://schema.org/>

SELECT ?manufacturerName
WHERE {
  ?thing schema:manufacturer ?manufacturer .
  ?manufacturer schema:name ?manufacturerName .
}
"""

# Execute the SPARQL query
results = g.query(query)

# Print the results
for row in results:
    print(row)

(rdflib.term.Literal('Sigma-Aldrich'),)

Fetch additional information from other sources [Advanced]#

Ontologies contain a lot of information about the meaning of things, but they don’t always contain an exhaustive list of all the properties. Instead, they often point to other sources where that information exists rather than duplicating it. Let’s see how you can use the ontology to fetch additional information from other sources.

First, we parse the ontology into the knowledge graph and retrieve the IRIs for the terms that we are interested in. In this case, we want to retrieve more information about Zinc from Wikidata, so we query the ontology to find Zinc’s Wikidata ID.

[108]:
# Parse the ontology into the knowledge graph
ontology = "https://raw.githubusercontent.com/emmo-repo/domain-electrochemistry/master/electrochemistry-inferred.ttl"
g.parse(ontology, format='turtle')

# Fetch the context
context_url = 'https://raw.githubusercontent.com/emmo-repo/domain-electrochemistry/master/context.json'
response = requests.get(context_url)
context_data = response.json()

# Look for the IRI of Zinc in the context
zinc_iri = context_data.get('@context', {}).get('Zinc')
wikidata_iri = context_data.get('@context', {}).get('wikidataReference')

# Query the ontology to find the wikidata id for zinc
query = """
SELECT ?wikidataId
WHERE {
    <%s> <%s> ?wikidataId .
}
""" % (zinc_iri, wikidata_iri)

qres = g.query(query)
for row in qres:
    wikidata_id = row.wikidataId.split('/')[-1]

print(f"The Wikidata ID of Zinc: {wikidata_id}")
The Wikidata ID of Zinc: Q758

Now that we have the Wikidata ID for Zinc, we can query their SPARQL endpoint to retrieve some property. Let’s ask it for the atomic mass.

[109]:
# Query the Wikidata knowledge graph for more information about zinc
wikidata_endpoint = "https://query.wikidata.org/sparql"

# SPARQL query to get the atomic mass of zinc (Q758)
query = """
SELECT ?mass WHERE {
  wd:%s wdt:P2067 ?mass .
}
""" % wikidata_id

# Execute the request
response = requests.get(wikidata_endpoint, params={'query': query, 'format': 'json'})
data = response.json()

# Extract and print the mass value
mass = data['results']['bindings'][0]['mass']['value']
print(f"Wikidata says the atomic mass of zinc is: {mass}")
Wikidata says the atomic mass of zinc is: 65.38

We can also retrieve more complex data. For example, let’s ask Wikidata to show us an image of zinc.

[110]:
# SPARQL query to get the image of zinc (Q758)
query = """
SELECT ?image WHERE {
  wd:%s wdt:P18 ?image .
}
""" % wikidata_id

# Execute the request
response = requests.get(wikidata_endpoint, params={'query': query, 'format': 'json'})
data = response.json()

# Extract and display the image URL
if data['results']['bindings']:
    image_url = data['results']['bindings'][0]['image']['value']
    print(f"Image of Zinc: {image_url}")
    display(Image(url=image_url, width=300))  # Adjust width and height as needed

else:
    print("No image found for Zinc.")
Image of Zinc: http://commons.wikimedia.org/wiki/Special:FilePath/Zinc%20fragment%20sublimed%20and%201cm3%20cube.jpg
[110]: