How do I find Entities that overlap another Entity Type in Python?


"Which pipes run through this building" and "which sensors sit inside this zone" are the same question: find the Entities of one Type that overlap the Entities of another.

Data Lab answers it with two selections. The primary selection describes what you are measuring against, the secondary selection describes what you want back, and a spatial criterion in the secondary compares each candidate against the primary. The secondary selection is the one that runs, so it is the set you get.

The library builds both selections for you. Build a query per Entity Type, pair them with secondary, and add the spatial criterion. Below, the matches are walked and then stamped with an attribute through a Change Set.


You will want to install our Python library to facilitate this process.

pip install bruce-models

Python
import bruce_models as BM

# What we are measuring against, and what we want back.
PRIMARY_TYPE_ID = "YOUR_CONTAINER_ENTITY_TYPE_ID"
SECONDARY_TYPE_ID = "YOUR_TARGET_ENTITY_TYPE_ID"

# The attribute stamped on each match, as path segments.
ATTR_PATH = ["inside_building"]

# Primary Entities compared per join.
BATCH = 50

# Either a session token from a login or a long-lived access token.
TOKEN = "YOUR_TOKEN"

bruce = BM.BruceApi({
    "account_id": "YOUR_ACCOUNT_ID",
    "session_id": TOKEN
})

containers = BM.DataLabBuilder(bruce).entity_type(PRIMARY_TYPE_ID)
targets = BM.DataLabBuilder(bruce).entity_type(SECONDARY_TYPE_ID)

# The secondary Entities that touch or overlap a primary.
# Use within_primary() instead for fully covered only.
overlapping = containers.secondary(targets, batch=BATCH).intersecting_primary()

print(f"{overlapping.count():,} matches")

changes = BM.ChangeSetBuilder(bruce)

def stamp(primary_id, secondaries):
    """
    Handles the matches for one primary Entity.
    """
    for entity in secondaries:
        values = {}
        BM.Entity.set_value(values, ATTR_PATH, primary_id)
        changes.queue(entity, values)

# An Entity inside two overlapping primaries is answered once per
# primary, and the queue merges those into one edit per Entity.
matched = overlapping.each_group(stamp)
print(f"{matched['primaries']:,} primaries matched something, "
      f"{matched['matches']:,} matches, {len(changes):,} to update")

report = changes.apply()
print(f"{report['succeeded']:,} updated, {report['failed_count']:,} failed")

Choosing the spatial comparison

Three attribute paths address an Entity's spatial data rather than one of your own attributes, and each answers a different question:

"Bruce"/"Boundaries" is the bounding box. Cheapest to compare, and the right choice when a box is a good enough answer, which it usually is for "roughly inside this building".

"Bruce"/"VectorGeometry" is the exact shape. Use it when the difference between the shape and its box matters, and expect it to cost more.

"Bruce"/"Location" is the single point. Use it with DISTANCETO and a Distance in metres for "within 50 metres of".

The spatial operators are INTERSECTS (the shapes touch or overlap), INSIDE and CONTAINS (the primary fully covers the candidate), BOUNDSOVERLAP (a fast, loose box overlap) and DISTANCETO.


Things worth knowing

The secondary selection is what comes back. Swapping which Type is primary and which is secondary changes the answer, not just the wording. Put the set you want to act on in the secondary.

Updating the set you are paging through. The example queues every edit during the walk and applies them afterwards, so the join cannot shift underneath the paging even when the change would stop a record matching.

Doing it all server side. If you only need to stamp an attribute on the matches, the pairing can do the whole thing without returning the records: overlapping.run_action_set_attribute(path, value). That returns a Pending Action to follow rather than a list, and avoids moving the Entities over the network at all. It also takes a Ref as the value, which is how you copy something off the primary onto each match.