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: site, equip, point, sensor, °F.

Lib

A versioned package grouping related defs. Examples: lib:ph (core), lib:phIoT (IoT extensions).

Namespace

A container that indexes all defs from one or more libs, providing symbol resolution and taxonomy queries.

Taxonomy

The is tag hierarchy defining subtype relationships. sensor is a subtype of point, which is a subtype of entity.

Conjunct

A compound term like hot-water composed from dash-separated parts.

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"]

Effective Tags

Compute the full tag set for a def, inheriting from all supertypes via effective_tags():

from hs_py.ontology.taxonomy import effective_tags

tags = effective_tags(ns, "sensor")
# Includes tags from sensor, point, entity, and all other ancestors
# Own tags take precedence over inherited tags

Marker Tags

Get the set of all marker tag names for a def and its supertypes via marker_tags():

from hs_py.ontology.taxonomy import marker_tags

markers = marker_tags(ns, "sensor")
# {"sensor", "point", "entity", ...}

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:

  1. Collect — gather all defs across all libs.

  2. Taxonify — for conjuncts like hot-water, add individual parts (hot, water) as supertypes if they exist as defs.

  3. Rebuild — reconstruct libs with updated defs.

  4. Build namespace — index all defs by name.

  5. 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:

  1. Scan — find all marker-valued tags in the entity dict.

  2. Match — look up each marker name as a def in the namespace.

  3. Conjuncts — check for conjunct defs whose parts are all present (e.g., if both hot and water markers exist, hot-water matches).

  4. 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.