Sponsored by AppSignal
Would you like to see your link here? Contact us
Notesclub

Resource Description Framework

rdf.livemd

Resource Description Framework

Mix.install([
  {:rdf, "~> 1.1"},
  {:sparql, "~> 0.3"},
  {:kino, "~> 0.10.0"}
])

Context

Links:

Convenience

import RDF.Sigils
alias RDF.NS.{RDFS}

Helper function to display resultsets in a pretty way:

result_as_table = fn ro ->
  cols = ro.variables
  results = ro.results

  lines =
    results
    |> List.foldl("", fn result, acc ->
      main =
        cols
        |> Enum.map(fn col -> "#{Map.get(result, col)}" end)
        |> Enum.join(" | ")

      acc <> "| #{main} |\n"
    end)

  Kino.Markdown.new("""
  | #{cols |> Enum.join(" | ")} |
  | #{cols |> Enum.map(fn _ -> "--" end) |> Enum.join(" | ")} |
  #{lines}
  """)
end

result_as_tree = fn ro ->
  cols = ro.variables
  results = ro.results

  contents =
    List.foldl(results, "graph LR;\n", fn result, acc ->
      src =
        Map.get(result, Enum.at(cols, 0))
        |> RDF.IRI.to_string()
        |> String.split("#")
        |> Enum.at(-1)

      dst =
        Map.get(result, Enum.at(cols, 1))
        |> RDF.IRI.to_string()
        |> String.split("#")
        |> Enum.at(-1)

      acc <> "  #{src}-->#{dst};\n"
    end)

  Kino.Mermaid.new(contents)
end

Schema

Vocabulary:

defmodule Schema do
  use RDF.Vocabulary.Namespace

  @base "http://sdu.dk/ss/courses/iot/2024/schemas/"

  defvocab(Data,
    base_iri: "#{@base}data#",
    terms: ~w[data Data hist live]
  )

  defvocab(Unit,
    base_iri: "#{@base}unit#",
    terms: ~w[unit Unit Unitless DegreesCelcius DegreesFahrenheit Kelvin Percentage]
  )

  defvocab(Location,
    base_iri: "#{@base}location#",
    terms: ~w[Location Building Floor Room contains area name adjacent]
  )

  defvocab(Modality,
    base_iri: "#{@base}modality#",
    terms: ~w[modality provides Modality Occupancy Temperature RelativeHumidity AbsoluteHumidity]
  )

  defvocab(Point,
    base_iri: "#{@base}point#",
    terms: ~w[Point Sensor PIR TemperatureSensor HumiditySensor]
  )
end

Relationships:

schema = [
  # data
  Schema.Data.Data
  |> RDFS.subClassOf(RDFS.Class),

  # unit
  Schema.Unit.Unit
  |> RDFS.subClassOf(RDFS.Class),
  Schema.Unit.Unitless
  |> RDFS.subClassOf(Schema.Unit.Unit),
  Schema.Unit.DegreesCelcius
  |> RDFS.subClassOf(Schema.Unit.Unit),
  Schema.Unit.DegreesFahrenheit
  |> RDFS.subClassOf(Schema.Unit.Unit),
  Schema.Unit.Kelvin
  |> RDFS.subClassOf(Schema.Unit.Unit),
  Schema.Unit.Percentage
  |> RDFS.subClassOf(Schema.Unit.Unit),

  # location
  Schema.Location.Location
  |> RDFS.subClassOf(RDFS.Class),
  Schema.Location.Building
  |> RDFS.subClassOf(Schema.Location.Location),
  Schema.Location.Floor
  |> RDFS.subClassOf(Schema.Location.Location),
  Schema.Location.Room
  |> RDFS.subClassOf(Schema.Location.Location),

  # modality
  Schema.Modality.Modality
  |> RDFS.subClassOf(RDFS.Class),
  Schema.Modality.Occupancy
  |> RDFS.subClassOf(Schema.Modality.Modality),
  Schema.Modality.Temperature
  |> RDFS.subClassOf(Schema.Modality.Modality),
  Schema.Modality.RelativeHumidity
  |> RDFS.subClassOf(Schema.Modality.Modality),
  Schema.Modality.AbsoluteHumidity
  |> RDFS.subClassOf(Schema.Modality.Modality),

  # point
  Schema.Point.Point
  |> RDFS.subClassOf(RDFS.Class),
  Schema.Point.Sensor
  |> RDFS.subClassOf(Schema.Point.Point),
  Schema.Point.PIR
  |> RDFS.subClassOf(Schema.Point.Sensor),
  Schema.Point.TemperatureSensor
  |> RDFS.subClassOf(Schema.Point.Sensor),
  Schema.Point.HumiditySensor
  |> RDFS.subClassOf(Schema.Point.Sensor)
]

Model

model = "http://www.sdu.dk#"

# namespaces to bind
ns = [
  rdfs: RDFS,
  data: Schema.Data,
  unit: Schema.Unit,
  location: Schema.Location,
  modality: Schema.Modality,
  point: Schema.Point,
  model: model
]

m = RDF.Graph.new(prefixes: ns)

Instances:

Variables for relevant entities:

floor = ~I"http://www.sdu.dk#floor3"
roomA = ~I"http://www.sdu.dk#roomA"
roomAocc = ~I"http://www.sdu.dk#roomA/occ"
roomApir = ~I"http://www.sdu.dk#roomA/pir"
roomApirData = ~I"http://www.sdu.dk#roomA/pir/data"
roomAtemp = ~I"http://www.sdu.dk#roomA/temp"
roomAtempData = ~I"http://www.sdu.dk#roomA/temp/data"
roomAtc = ~I"http://www.sdu.dk#roomA/tc"
roomAtcData = ~I"http://www.sdu.dk#roomA/tc/data"
roomArhum = ~I"http://www.sdu.dk#roomA/rhum"
roomAhum = ~I"http://www.sdu.dk#roomA/hum"
roomAhumData = ~I"http://www.sdu.dk#roomA/hum/data"
roomB = ~I"http://www.sdu.dk#roomB"
roomBocc = ~I"http://www.sdu.dk#roomB/occ"
roomBpir = ~I"http://www.sdu.dk#roomB/pir"
roomBpirData = ~I"http://www.sdu.dk#roomB/pir/data"
roomBtemp = ~I"http://www.sdu.dk#roomB/temp"
roomBtempData = ~I"http://www.sdu.dk#roomA/temp/data"
roomBtc = ~I"http://www.sdu.dk#roomB/tc"
roomBtcData = ~I"http://www.sdu.dk#roomB/tc/data"
roomBrhum = ~I"http://www.sdu.dk#roomB/rhum"
roomBhum = ~I"http://www.sdu.dk#roomB/hum"
roomBhumData = ~I"http://www.sdu.dk#roomB/hum/data"

Add relationships:

model =
  schema ++
    [
      # floors
      floor
      |> RDF.type(Schema.Location.Floor)
      |> Schema.Location.name(~L"3rd")
      |> Schema.Location.contains(roomA)
      |> Schema.Location.contains(roomB),

      # rooms
      roomA
      |> RDF.type(Schema.Location.Room)
      |> Schema.Location.area(~L"17")
      |> Schema.Modality.modality(roomAocc)
      |> Schema.Modality.modality(roomAtemp)
      |> Schema.Modality.modality(roomArhum)
      |> Schema.Location.adjacent(roomB),
      roomB
      |> RDF.type(Schema.Location.Room)
      |> Schema.Location.area(~L"23")
      |> Schema.Modality.modality(roomBocc)
      |> Schema.Modality.modality(roomBtemp)
      |> Schema.Modality.modality(roomBrhum)
      |> Schema.Location.adjacent(roomA),

      # modalities
      roomAocc
      |> RDF.type(Schema.Modality.Occupancy),
      roomAtemp
      |> RDF.type(Schema.Modality.Temperature),
      roomArhum
      |> RDF.type(Schema.Modality.RelativeHumidity),
      roomBocc
      |> RDF.type(Schema.Modality.Occupancy),
      roomBtemp
      |> RDF.type(Schema.Modality.Temperature),
      roomBrhum
      |> RDF.type(Schema.Modality.RelativeHumidity),

      # pir sensors
      roomApir
      |> RDF.type(Schema.Point.PIR)
      |> Schema.Unit.unit(Schema.Unit.Unitless)
      |> Schema.Data.data(roomApirData)
      |> Schema.Modality.provides(roomAocc),
      roomBpir
      |> RDF.type(Schema.Point.PIR)
      |> Schema.Unit.unit(Schema.Unit.Unitless)
      |> Schema.Data.data(roomBpirData)
      |> Schema.Modality.provides(roomBocc),

      # temperature sensors
      roomAtemp
      |> RDF.type(Schema.Point.TemperatureSensor)
      |> Schema.Unit.unit(Schema.Unit.DegreesCelcius)
      |> Schema.Data.data(roomAtempData)
      |> Schema.Modality.provides(roomAtemp),
      roomBtemp
      |> RDF.type(Schema.Point.TemperatureSensor)
      |> Schema.Unit.unit(Schema.Unit.Kelvin)
      |> Schema.Data.data(roomBtempData)
      |> Schema.Modality.provides(roomBtemp),

      # humidity sensors
      roomAhum
      |> RDF.type(Schema.Point.HumiditySensor)
      |> Schema.Unit.unit(Schema.Unit.Percentage)
      |> Schema.Data.data(roomAhumData)
      |> Schema.Modality.provides(roomArhum),
      roomBhum
      |> RDF.type(Schema.Point.HumiditySensor)
      |> Schema.Unit.unit(Schema.Unit.Percentage)
      |> Schema.Data.data(roomBhumData)
      |> Schema.Modality.provides(roomBrhum),

      # data
      roomApirData
      |> RDF.type(Schema.Data.Data)
      |> Schema.Data.hist(~L"24")
      |> Schema.Data.live(~L"5a3573c3"),
      roomBpirData
      |> RDF.type(Schema.Data.Data)
      |> Schema.Data.hist(~L"28")
      |> Schema.Data.live(~L"92d17015"),
      roomAtempData
      |> RDF.type(Schema.Data.Data)
      |> Schema.Data.hist(~L"12")
      |> Schema.Data.live(~L"17c5ae15"),
      roomBtempData
      |> RDF.type(Schema.Data.Data)
      |> Schema.Data.hist(~L"37")
      |> Schema.Data.live(~L"b0bc97af"),
      roomAhumData
      |> RDF.type(Schema.Data.Data)
      |> Schema.Data.hist(~L"13")
      |> Schema.Data.live(~L"65d0be73"),
      roomBhumData
      |> RDF.type(Schema.Data.Data)
      |> Schema.Data.hist(~L"22")
      |> Schema.Data.live(~L"06ef2490")
    ]
model = model |> RDF.Graph.new() |> RDF.Graph.add_prefixes(ns)

Serialize

If we want to store the file to disk:

model |> RDF.Turtle.write_string!() |> IO.puts()

Queries

Room sizes:

q = "
  PREFIX location: 
  
  SELECT ?area
  WHERE {
    ?room a location:Room .
    ?room location:area ?area .
  }
"

SPARQL.execute_query(model, q)
|> result_as_table.()

Per-floor combinations of temperature and relative humidity data:

q = "
  PREFIX location: 
  PREFIX modality: 
  
  SELECT ?name ?temp ?rhum
  WHERE {
    ?floor a location:Floor .
    ?room a location:Room .
    ?temp_mod a modality:Temperature .
    ?rhum_mod a modality:RelativeHumidity .

    ?floor location:name ?name .
    ?floor location:contains ?room .

    ?room modality:modality ?temp_mod .
    ?temp modality:provides ?temp_mod .

    ?room modality:modality ?rhum_mod .
    ?rhum modality:provides ?rhum_mod .
  }
"

SPARQL.execute_query(model, q)
|> result_as_table.()

Class hierarchy:

q = "
  SELECT ?sub ?super
  WHERE {
    ?sub rdfs:subClassOf ?super .
  }
"

SPARQL.execute_query(model, q)
|> result_as_tree.()