Skip to content

References

ReferenceGraph maintains a bidirectional index of cross-object references. It powers doc.get_referencing() and doc.get_references() and is automatically kept in sync as objects are added, removed, or renamed.

Reference graph for tracking object dependencies.

Provides O(1) lookups for: - What objects reference a given name? - What names does an object reference? - Validation of reference integrity

ReferenceGraph

Tracks object references for instant dependency queries.

The graph maintains two indexes: - _referenced_by: name -> set of objects that reference it - _references: object -> set of names it references

This enables O(1) lookups for common operations like: - Finding all surfaces in a zone - Finding all objects using a construction - Detecting dangling references

The reference graph is automatically maintained by IDFDocument when objects are added, removed, or when reference fields are modified.

Examples:

The reference graph automatically tracks which objects point to which names. For instance, when a surface references a zone, that link is available for instant queries:

>>> from idfkit import new_document
>>> model = new_document()
>>> model.add("Zone", "Perimeter_ZN_1")
Zone('Perimeter_ZN_1')
>>> model.add("BuildingSurface:Detailed", "South_Wall",
...     surface_type="Wall", construction_name="",
...     zone_name="Perimeter_ZN_1",
...     outside_boundary_condition="Outdoors",
...     sun_exposure="SunExposed", wind_exposure="WindExposed",
...     validate=False)
BuildingSurface:Detailed('South_Wall')
>>> model.references.is_referenced("Perimeter_ZN_1")
True
>>> stats = model.references.stats()
>>> stats["total_references"] >= 1
True
Source code in src/idfkit/references.py
class ReferenceGraph:
    """
    Tracks object references for instant dependency queries.

    The graph maintains two indexes:
    - _referenced_by: name -> set of objects that reference it
    - _references: object -> set of names it references

    This enables O(1) lookups for common operations like:
    - Finding all surfaces in a zone
    - Finding all objects using a construction
    - Detecting dangling references

    The reference graph is automatically maintained by
    [IDFDocument][idfkit.document.IDFDocument] when objects are added,
    removed, or when reference fields are modified.

    Examples:
        The reference graph automatically tracks which objects point
        to which names.  For instance, when a surface references a
        zone, that link is available for instant queries:

        >>> from idfkit import new_document
        >>> model = new_document()
        >>> model.add("Zone", "Perimeter_ZN_1")  # doctest: +ELLIPSIS
        Zone('Perimeter_ZN_1')
        >>> model.add("BuildingSurface:Detailed", "South_Wall",
        ...     surface_type="Wall", construction_name="",
        ...     zone_name="Perimeter_ZN_1",
        ...     outside_boundary_condition="Outdoors",
        ...     sun_exposure="SunExposed", wind_exposure="WindExposed",
        ...     validate=False)  # doctest: +ELLIPSIS
        BuildingSurface:Detailed('South_Wall')
        >>> model.references.is_referenced("Perimeter_ZN_1")
        True
        >>> stats = model.references.stats()
        >>> stats["total_references"] >= 1
        True
    """

    __slots__ = ("_object_lists", "_referenced_by", "_references")

    def __init__(self) -> None:
        # name (uppercase) -> set of (object, field_name) tuples that reference it
        self._referenced_by: dict[str, set[tuple[IDFObject, str]]] = defaultdict(set)
        # object -> set of (name_uppercase, field_name) tuples it references
        self._references: dict[IDFObject, set[tuple[str, str]]] = defaultdict(set)
        # object_list name -> set of object types that provide names for it
        self._object_lists: dict[str, set[str]] = defaultdict(set)

    def register_object_list(self, list_name: str, obj_type: str) -> None:
        """Register that an object type provides names for an object-list."""
        self._object_lists[list_name].add(obj_type)

    def register(self, obj: IDFObject, field_name: str, referenced_name: str) -> None:
        """
        Register that an object references another name.

        Args:
            obj: The object that contains the reference
            field_name: The field that contains the reference
            referenced_name: The name being referenced
        """
        if not referenced_name:
            return

        name_upper = referenced_name.upper()
        self._referenced_by[name_upper].add((obj, field_name))
        self._references[obj].add((name_upper, field_name))

    def unregister(self, obj: IDFObject) -> None:
        """Remove all reference tracking for an object."""
        if obj in self._references:
            # Remove from referenced_by
            for name_upper, field_name in self._references[obj]:
                if name_upper in self._referenced_by:
                    self._referenced_by[name_upper].discard((obj, field_name))
                    if not self._referenced_by[name_upper]:
                        del self._referenced_by[name_upper]
            del self._references[obj]

        # Also remove any references TO this object
        obj_name_upper = obj.name.upper() if obj.name else ""
        if obj_name_upper in self._referenced_by:
            del self._referenced_by[obj_name_upper]

    def get_referencing(self, name: str) -> set[IDFObject]:
        """
        O(1): Get all objects that reference a given name.

        Args:
            name: The name to look up

        Returns:
            Set of IDFObjects that reference this name

        Examples:
            Find all surfaces assigned to a zone (O(1)):

            >>> from idfkit import new_document
            >>> model = new_document()
            >>> model.add("Zone", "Perimeter_ZN_1")  # doctest: +ELLIPSIS
            Zone('Perimeter_ZN_1')
            >>> model.add("BuildingSurface:Detailed", "South_Wall",
            ...     surface_type="Wall", construction_name="",
            ...     zone_name="Perimeter_ZN_1",
            ...     outside_boundary_condition="Outdoors",
            ...     sun_exposure="SunExposed", wind_exposure="WindExposed",
            ...     validate=False)  # doctest: +ELLIPSIS
            BuildingSurface:Detailed('South_Wall')
            >>> len(model.references.get_referencing("Perimeter_ZN_1"))
            1
        """
        refs = self._referenced_by.get(name.upper(), set())
        return {obj for obj, _ in refs}

    def get_referencing_with_fields(self, name: str) -> set[tuple[IDFObject, str]]:
        """
        O(1): Get all (object, field_name) pairs that reference a given name.

        Args:
            name: The name to look up

        Returns:
            Set of (IDFObject, field_name) tuples
        """
        return self._referenced_by.get(name.upper(), set()).copy()

    def get_references(self, obj: IDFObject) -> set[str]:
        """
        O(1): Get all names that an object references.

        Args:
            obj: The object to look up

        Returns:
            Set of names (uppercase) that this object references
        """
        refs = self._references.get(obj, set())
        return {name for name, _ in refs}

    def get_references_with_fields(self, obj: IDFObject) -> set[tuple[str, str]]:
        """
        O(1): Get all (name, field_name) pairs that an object references.

        Args:
            obj: The object to look up

        Returns:
            Set of (name, field_name) tuples
        """
        return self._references.get(obj, set()).copy()

    def is_referenced(self, name: str) -> bool:
        """Check if a name is referenced by any object.

        Examples:
            Check whether a zone is used by any surface before deleting it:

            >>> from idfkit import new_document
            >>> model = new_document()
            >>> model.add("Zone", "Perimeter_ZN_1")  # doctest: +ELLIPSIS
            Zone('Perimeter_ZN_1')
            >>> model.references.is_referenced("Perimeter_ZN_1")
            False
            >>> model.add("BuildingSurface:Detailed", "South_Wall",
            ...     surface_type="Wall", construction_name="",
            ...     zone_name="Perimeter_ZN_1",
            ...     outside_boundary_condition="Outdoors",
            ...     sun_exposure="SunExposed", wind_exposure="WindExposed",
            ...     validate=False)  # doctest: +ELLIPSIS
            BuildingSurface:Detailed('South_Wall')
            >>> model.references.is_referenced("Perimeter_ZN_1")
            True
        """
        return name.upper() in self._referenced_by

    def get_dangling_references(self, valid_names: set[str]) -> Iterator[tuple[IDFObject, str, str]]:
        """
        Find all references to non-existent objects.

        Args:
            valid_names: Set of valid object names (uppercase)

        Yields:
            Tuples of (source_object, field_name, referenced_name)
        """
        valid_upper = {n.upper() for n in valid_names}

        for obj, refs in self._references.items():
            for name_upper, field_name in refs:
                if name_upper not in valid_upper:
                    yield (obj, field_name, name_upper)

    def rename_target(self, old_name: str, new_name: str) -> None:
        """
        Update indexes when a referenced target is renamed.

        Moves _referenced_by[OLD] -> _referenced_by[NEW] and updates
        corresponding _references entries for all affected objects.

        Args:
            old_name: The old target name
            new_name: The new target name
        """
        old_upper = old_name.upper()
        new_upper = new_name.upper()
        if old_upper == new_upper:
            return

        referrers = self._referenced_by.pop(old_upper, set())
        if not referrers:
            return

        # Update _references for each referring object
        for obj, field_name in referrers:
            obj_refs = self._references.get(obj)
            if obj_refs is not None:
                obj_refs.discard((old_upper, field_name))
                obj_refs.add((new_upper, field_name))

        # Merge into new key (there may already be refs to new_name)
        if new_upper in self._referenced_by:
            self._referenced_by[new_upper].update(referrers)
        else:
            self._referenced_by[new_upper] = referrers

    def update_reference(self, obj: IDFObject, field_name: str, old_value: str | None, new_value: str | None) -> None:
        """
        Update indexes when an object's reference field changes.

        Removes the old entry from both indexes and adds the new entry.

        Args:
            obj: The object whose field changed
            field_name: The field that changed
            old_value: The previous referenced name (or None)
            new_value: The new referenced name (or None)
        """
        # Remove old
        if old_value:
            old_upper = old_value.upper()
            refs_set = self._referenced_by.get(old_upper)
            if refs_set is not None:
                refs_set.discard((obj, field_name))
                if not refs_set:
                    del self._referenced_by[old_upper]
            obj_refs = self._references.get(obj)
            if obj_refs is not None:
                obj_refs.discard((old_upper, field_name))

        # Add new
        if new_value and new_value.strip():
            new_upper = new_value.upper()
            self._referenced_by[new_upper].add((obj, field_name))
            self._references[obj].add((new_upper, field_name))

    def clear(self) -> None:
        """Clear all reference tracking."""
        self._referenced_by.clear()
        self._references.clear()
        self._object_lists.clear()

    def __len__(self) -> int:
        """Return total number of references tracked."""
        return sum(len(refs) for refs in self._references.values())

    def stats(self) -> dict[str, int]:
        """Return statistics about the reference graph."""
        return {
            "total_references": len(self),
            "objects_with_references": len(self._references),
            "names_referenced": len(self._referenced_by),
            "object_lists": len(self._object_lists),
        }

__len__()

Return total number of references tracked.

Source code in src/idfkit/references.py
def __len__(self) -> int:
    """Return total number of references tracked."""
    return sum(len(refs) for refs in self._references.values())

clear()

Clear all reference tracking.

Source code in src/idfkit/references.py
def clear(self) -> None:
    """Clear all reference tracking."""
    self._referenced_by.clear()
    self._references.clear()
    self._object_lists.clear()

get_dangling_references(valid_names)

Find all references to non-existent objects.

Parameters:

Name Type Description Default
valid_names set[str]

Set of valid object names (uppercase)

required

Yields:

Type Description
tuple[IDFObject, str, str]

Tuples of (source_object, field_name, referenced_name)

Source code in src/idfkit/references.py
def get_dangling_references(self, valid_names: set[str]) -> Iterator[tuple[IDFObject, str, str]]:
    """
    Find all references to non-existent objects.

    Args:
        valid_names: Set of valid object names (uppercase)

    Yields:
        Tuples of (source_object, field_name, referenced_name)
    """
    valid_upper = {n.upper() for n in valid_names}

    for obj, refs in self._references.items():
        for name_upper, field_name in refs:
            if name_upper not in valid_upper:
                yield (obj, field_name, name_upper)

get_references(obj)

O(1): Get all names that an object references.

Parameters:

Name Type Description Default
obj IDFObject

The object to look up

required

Returns:

Type Description
set[str]

Set of names (uppercase) that this object references

Source code in src/idfkit/references.py
def get_references(self, obj: IDFObject) -> set[str]:
    """
    O(1): Get all names that an object references.

    Args:
        obj: The object to look up

    Returns:
        Set of names (uppercase) that this object references
    """
    refs = self._references.get(obj, set())
    return {name for name, _ in refs}

get_references_with_fields(obj)

O(1): Get all (name, field_name) pairs that an object references.

Parameters:

Name Type Description Default
obj IDFObject

The object to look up

required

Returns:

Type Description
set[tuple[str, str]]

Set of (name, field_name) tuples

Source code in src/idfkit/references.py
def get_references_with_fields(self, obj: IDFObject) -> set[tuple[str, str]]:
    """
    O(1): Get all (name, field_name) pairs that an object references.

    Args:
        obj: The object to look up

    Returns:
        Set of (name, field_name) tuples
    """
    return self._references.get(obj, set()).copy()

get_referencing(name)

O(1): Get all objects that reference a given name.

Parameters:

Name Type Description Default
name str

The name to look up

required

Returns:

Type Description
set[IDFObject]

Set of IDFObjects that reference this name

Examples:

Find all surfaces assigned to a zone (O(1)):

>>> from idfkit import new_document
>>> model = new_document()
>>> model.add("Zone", "Perimeter_ZN_1")
Zone('Perimeter_ZN_1')
>>> model.add("BuildingSurface:Detailed", "South_Wall",
...     surface_type="Wall", construction_name="",
...     zone_name="Perimeter_ZN_1",
...     outside_boundary_condition="Outdoors",
...     sun_exposure="SunExposed", wind_exposure="WindExposed",
...     validate=False)
BuildingSurface:Detailed('South_Wall')
>>> len(model.references.get_referencing("Perimeter_ZN_1"))
1
Source code in src/idfkit/references.py
def get_referencing(self, name: str) -> set[IDFObject]:
    """
    O(1): Get all objects that reference a given name.

    Args:
        name: The name to look up

    Returns:
        Set of IDFObjects that reference this name

    Examples:
        Find all surfaces assigned to a zone (O(1)):

        >>> from idfkit import new_document
        >>> model = new_document()
        >>> model.add("Zone", "Perimeter_ZN_1")  # doctest: +ELLIPSIS
        Zone('Perimeter_ZN_1')
        >>> model.add("BuildingSurface:Detailed", "South_Wall",
        ...     surface_type="Wall", construction_name="",
        ...     zone_name="Perimeter_ZN_1",
        ...     outside_boundary_condition="Outdoors",
        ...     sun_exposure="SunExposed", wind_exposure="WindExposed",
        ...     validate=False)  # doctest: +ELLIPSIS
        BuildingSurface:Detailed('South_Wall')
        >>> len(model.references.get_referencing("Perimeter_ZN_1"))
        1
    """
    refs = self._referenced_by.get(name.upper(), set())
    return {obj for obj, _ in refs}

get_referencing_with_fields(name)

O(1): Get all (object, field_name) pairs that reference a given name.

Parameters:

Name Type Description Default
name str

The name to look up

required

Returns:

Type Description
set[tuple[IDFObject, str]]

Set of (IDFObject, field_name) tuples

Source code in src/idfkit/references.py
def get_referencing_with_fields(self, name: str) -> set[tuple[IDFObject, str]]:
    """
    O(1): Get all (object, field_name) pairs that reference a given name.

    Args:
        name: The name to look up

    Returns:
        Set of (IDFObject, field_name) tuples
    """
    return self._referenced_by.get(name.upper(), set()).copy()

is_referenced(name)

Check if a name is referenced by any object.

Examples:

Check whether a zone is used by any surface before deleting it:

>>> from idfkit import new_document
>>> model = new_document()
>>> model.add("Zone", "Perimeter_ZN_1")
Zone('Perimeter_ZN_1')
>>> model.references.is_referenced("Perimeter_ZN_1")
False
>>> model.add("BuildingSurface:Detailed", "South_Wall",
...     surface_type="Wall", construction_name="",
...     zone_name="Perimeter_ZN_1",
...     outside_boundary_condition="Outdoors",
...     sun_exposure="SunExposed", wind_exposure="WindExposed",
...     validate=False)
BuildingSurface:Detailed('South_Wall')
>>> model.references.is_referenced("Perimeter_ZN_1")
True
Source code in src/idfkit/references.py
def is_referenced(self, name: str) -> bool:
    """Check if a name is referenced by any object.

    Examples:
        Check whether a zone is used by any surface before deleting it:

        >>> from idfkit import new_document
        >>> model = new_document()
        >>> model.add("Zone", "Perimeter_ZN_1")  # doctest: +ELLIPSIS
        Zone('Perimeter_ZN_1')
        >>> model.references.is_referenced("Perimeter_ZN_1")
        False
        >>> model.add("BuildingSurface:Detailed", "South_Wall",
        ...     surface_type="Wall", construction_name="",
        ...     zone_name="Perimeter_ZN_1",
        ...     outside_boundary_condition="Outdoors",
        ...     sun_exposure="SunExposed", wind_exposure="WindExposed",
        ...     validate=False)  # doctest: +ELLIPSIS
        BuildingSurface:Detailed('South_Wall')
        >>> model.references.is_referenced("Perimeter_ZN_1")
        True
    """
    return name.upper() in self._referenced_by

register(obj, field_name, referenced_name)

Register that an object references another name.

Parameters:

Name Type Description Default
obj IDFObject

The object that contains the reference

required
field_name str

The field that contains the reference

required
referenced_name str

The name being referenced

required
Source code in src/idfkit/references.py
def register(self, obj: IDFObject, field_name: str, referenced_name: str) -> None:
    """
    Register that an object references another name.

    Args:
        obj: The object that contains the reference
        field_name: The field that contains the reference
        referenced_name: The name being referenced
    """
    if not referenced_name:
        return

    name_upper = referenced_name.upper()
    self._referenced_by[name_upper].add((obj, field_name))
    self._references[obj].add((name_upper, field_name))

register_object_list(list_name, obj_type)

Register that an object type provides names for an object-list.

Source code in src/idfkit/references.py
def register_object_list(self, list_name: str, obj_type: str) -> None:
    """Register that an object type provides names for an object-list."""
    self._object_lists[list_name].add(obj_type)

rename_target(old_name, new_name)

Update indexes when a referenced target is renamed.

Moves _referenced_by[OLD] -> _referenced_by[NEW] and updates corresponding _references entries for all affected objects.

Parameters:

Name Type Description Default
old_name str

The old target name

required
new_name str

The new target name

required
Source code in src/idfkit/references.py
def rename_target(self, old_name: str, new_name: str) -> None:
    """
    Update indexes when a referenced target is renamed.

    Moves _referenced_by[OLD] -> _referenced_by[NEW] and updates
    corresponding _references entries for all affected objects.

    Args:
        old_name: The old target name
        new_name: The new target name
    """
    old_upper = old_name.upper()
    new_upper = new_name.upper()
    if old_upper == new_upper:
        return

    referrers = self._referenced_by.pop(old_upper, set())
    if not referrers:
        return

    # Update _references for each referring object
    for obj, field_name in referrers:
        obj_refs = self._references.get(obj)
        if obj_refs is not None:
            obj_refs.discard((old_upper, field_name))
            obj_refs.add((new_upper, field_name))

    # Merge into new key (there may already be refs to new_name)
    if new_upper in self._referenced_by:
        self._referenced_by[new_upper].update(referrers)
    else:
        self._referenced_by[new_upper] = referrers

stats()

Return statistics about the reference graph.

Source code in src/idfkit/references.py
def stats(self) -> dict[str, int]:
    """Return statistics about the reference graph."""
    return {
        "total_references": len(self),
        "objects_with_references": len(self._references),
        "names_referenced": len(self._referenced_by),
        "object_lists": len(self._object_lists),
    }

unregister(obj)

Remove all reference tracking for an object.

Source code in src/idfkit/references.py
def unregister(self, obj: IDFObject) -> None:
    """Remove all reference tracking for an object."""
    if obj in self._references:
        # Remove from referenced_by
        for name_upper, field_name in self._references[obj]:
            if name_upper in self._referenced_by:
                self._referenced_by[name_upper].discard((obj, field_name))
                if not self._referenced_by[name_upper]:
                    del self._referenced_by[name_upper]
        del self._references[obj]

    # Also remove any references TO this object
    obj_name_upper = obj.name.upper() if obj.name else ""
    if obj_name_upper in self._referenced_by:
        del self._referenced_by[obj_name_upper]

update_reference(obj, field_name, old_value, new_value)

Update indexes when an object's reference field changes.

Removes the old entry from both indexes and adds the new entry.

Parameters:

Name Type Description Default
obj IDFObject

The object whose field changed

required
field_name str

The field that changed

required
old_value str | None

The previous referenced name (or None)

required
new_value str | None

The new referenced name (or None)

required
Source code in src/idfkit/references.py
def update_reference(self, obj: IDFObject, field_name: str, old_value: str | None, new_value: str | None) -> None:
    """
    Update indexes when an object's reference field changes.

    Removes the old entry from both indexes and adds the new entry.

    Args:
        obj: The object whose field changed
        field_name: The field that changed
        old_value: The previous referenced name (or None)
        new_value: The new referenced name (or None)
    """
    # Remove old
    if old_value:
        old_upper = old_value.upper()
        refs_set = self._referenced_by.get(old_upper)
        if refs_set is not None:
            refs_set.discard((obj, field_name))
            if not refs_set:
                del self._referenced_by[old_upper]
        obj_refs = self._references.get(obj)
        if obj_refs is not None:
            obj_refs.discard((old_upper, field_name))

    # Add new
    if new_value and new_value.strip():
        new_upper = new_value.upper()
        self._referenced_by[new_upper].add((obj, field_name))
        self._references[obj].add((new_upper, field_name))