Ontology¶
The ontology module implements the Project Haystack definition model — a structured type system for describing building equipment, sensors, and their relationships. It supports parsing ontology definitions from Trio files, compiling them into a resolved namespace, querying the subtype hierarchy, and reflecting entity dicts against the ontology.
See also
Ontology for the full ontology API reference (defs, namespace, taxonomy, normalization, reflection).
Concepts¶
The Haystack ontology is built on a few core concepts:
Concept |
Description |
|---|---|
Def |
A named definition (term) in the ontology, carrying metadata tags.
Examples: |
Lib |
A versioned package grouping related defs.
Examples: |
Namespace |
A container that indexes all defs from one or more libs, providing symbol resolution and taxonomy queries. |
Taxonomy |
The |
Conjunct |
A compound term like |
Reflection |
Determining which defs apply to an entity based on its marker tags. |
Defs and Libs¶
A Def represents a single ontology term:
from hs_py.ontology.defs import Def
from hs_py.kinds import Symbol, MARKER
site_def = Def(
symbol=Symbol("site"),
tags={
"def": Symbol("site"),
"is": Symbol("entity"),
"doc": "A geographic site such as a campus or building",
"marker": MARKER,
},
)
print(site_def.name) # "site"
print(site_def.doc) # "A geographic site..."
print(site_def.is_list) # [Symbol("entity")]
A Lib groups defs into a distributable package:
from hs_py.ontology.defs import Lib
from hs_py.kinds import Symbol
lib = Lib(
symbol=Symbol("lib:ph"),
version="4.0",
defs=(site_def, equip_def, point_def),
)
Loading from Trio Files¶
Ontology definitions are typically distributed as Trio text files. Use the loading helpers to parse them:
from hs_py.ontology.namespace import load_defs_from_trio, load_lib_from_trio
# Parse individual defs from Trio text
defs = load_defs_from_trio("""
def: ^site
is: ^entity
doc: "A geographic site"
def: ^equip
is: ^entity
doc: "A physical equipment asset"
def: ^point
is: ^entity
doc: "A data point"
""")
# Load a complete lib (lib metadata + defs)
lib = load_lib_from_trio(
lib_trio='def: ^lib:myLib\nversion: "1.0"',
def_trios=[open("defs.trio").read()],
)
See Wire Formats for Trio format details.
Namespace¶
The Namespace indexes defs from one or
more libs, providing fast lookup and taxonomy queries:
from hs_py.ontology.namespace import Namespace
ns = Namespace(libs=[lib_ph, lib_phIoT])
# Lookup by name
site = ns.get("site")
assert site is not None
print(site.doc)
# Check existence
assert ns.has("point")
assert not ns.has("nonexistent")
# Count
print(f"{ns.def_count} defs loaded")
Qualified vs Unqualified Names¶
Defs can be looked up by both qualified (ph::site) and unqualified
(site) names. When multiple libs define the same unqualified name,
the first registered lib wins:
site_q = ns.get("ph::site") # Qualified lookup
site_u = ns.get("site") # Unqualified lookup
assert site_q is site_u # Same Def object
Iterating¶
# All unique defs
for d in ns.all_defs():
print(d.symbol.val, d.doc)
# All libs
for lib in ns.all_libs():
print(lib.symbol.val, lib.version)
Taxonomy¶
The taxonomy is the is tag hierarchy. Every def declares its supertypes
via the is tag, forming a tree:
entity
├── site
├── equip
└── point
├── sensor
├── cmd
├── sp
└── weather
Querying the Hierarchy¶
# Direct subtypes
point_subs = ns.subtypes("point")
for d in point_subs:
print(d.name) # sensor, cmd, sp, weather, ...
# Direct supertypes
parents = ns.supertypes("sensor")
for d in parents:
print(d.name) # point
# All transitive supertypes (cached)
all_parents = ns.all_supertypes("sensor")
for d in all_parents:
print(d.name) # point, entity, ...
# Transitive subtype check
assert ns.is_subtype("sensor", "point")
assert ns.is_subtype("sensor", "entity")
assert not ns.is_subtype("site", "point")
# Identity — a def is a subtype of itself
assert ns.is_subtype("sensor", "sensor")
Conjuncts¶
Conjuncts are compound terms like hot-water or chilled-water-plant.
They are composed from dash-separated parts, each of which must be a valid
def:
from hs_py.ontology.taxonomy import is_conjunct, resolve_conjunct_parts
assert is_conjunct("hot-water")
assert not is_conjunct("sensor")
parts = resolve_conjunct_parts("hot-water-plant")
# ["hot", "water", "plant"]
Tag-On Mapping¶
Find which entity defs a tag is declared tagOn via
tag_on_defs():
from hs_py.ontology.taxonomy import tag_on_defs
entities = tag_on_defs(ns, "curVal")
# ["point"] — curVal is tagOn point
Normalization¶
The normalization pipeline compile_namespace()
compiles raw libs into a fully resolved namespace. It handles conjunct
supertype generation, validation of missing references, and cycle detection.
from hs_py.ontology.normalize import compile_namespace, NormalizeError
ns = compile_namespace([lib_ph, lib_phIoT])
# Returns a fully validated Namespace
# If there are errors, NormalizeError is raised:
try:
ns = compile_namespace([broken_lib])
except NormalizeError as e:
print(f"Normalization failed: {e}")
# Causes include: missing supertypes, cycles in the is-hierarchy
The pipeline steps are:
Collect — gather all defs across all libs.
Taxonify — for conjuncts like
hot-water, add individual parts (hot,water) as supertypes if they exist as defs.Rebuild — reconstruct libs with updated defs.
Build namespace — index all defs by name.
Validate — check for missing supertypes and cycles.
Reflection¶
Reflection determines which defs apply to an entity based on its marker tags. This is how you answer “what kind of thing is this record?”
from hs_py.ontology.reflect import reflect, fits
from hs_py.kinds import MARKER, Number, Ref
entity = {
"id": Ref("p1"),
"dis": "Zone Temp",
"point": MARKER,
"sensor": MARKER,
"temp": MARKER,
"zone": MARKER,
"curVal": Number(72.5, "°F"),
}
# Get all applicable defs (most-specific first)
defs = reflect(ns, entity)
for d in defs:
print(d.name)
# sensor, temp, zone, point, entity, ...
# (includes conjuncts and all transitive supertypes)
# Check if an entity fits a specific def
assert fits(ns, entity, "sensor")
assert fits(ns, entity, "point")
assert fits(ns, entity, "entity")
assert not fits(ns, entity, "equip")
Reflection Algorithm¶
The reflect() algorithm:
Scan — find all marker-valued tags in the entity dict.
Match — look up each marker name as a def in the namespace.
Conjuncts — check for conjunct defs whose parts are all present (e.g., if both
hotandwatermarkers exist,hot-watermatches).Supertypes — collect all transitive supertypes of matched defs.
The result is ordered most-specific first: direct marker matches appear before their ancestors in the hierarchy.