Query API¶
The Query API provides a developer-friendly interface for querying pyDEXPI models. It sits on top of the existing object model, toolkit, and NetworkX graph functionality — it does not replace any existing parsing or graph logic.
Quick Start¶
from pydexpi.loaders import DexpiXmlSerializer
model = DexpiXmlSerializer.read_dexpi_xml("my_plant.xml")
# Access the query engine via model.q or use convenience methods directly
pump = model.get_by_tag("P-101")
tanks = model.find_all(type="Tank")
connections = model.find_connections("P-101")
paths = model.find_paths("TK-101", "P-201", max_depth=8)
API Reference¶
Object Lookup¶
get_by_id(object_id)¶
Look up a single object by its unique ID. Raises ObjectNotFoundError if no object has the given ID.
find_by_tag(tag, *, type=None)¶
Find all objects with a given tag. Returns a list (possibly empty). Tags are resolved by
pydexpi.toolkits.display_name.get_tag, the same resolution behind display names:
tagName for equipment, subTagName for nozzles, lineNumber for piping network systems,
pipingComponentName for valves, identifier for the objects of a DEXPI 2.0 process model
(process steps, ports, streams and other process connections, materials), and so on. A process
object's free-text label is never a tag.
Instrumentation functions also have their printed tag (e.g. PI-4712.01) indexed as an
alias. Their canonical tag remains the bare number, which may also name a loop and other
instruments. The optional type filter matches concrete and DEXPI parent class names,
case-sensitively, just as find_all does. Unknown types return no matches.
get_by_tag(tag, *, type=None)¶
Get exactly one object by tag. Raises ObjectNotFoundError if no match, or AmbiguousTagError if multiple objects share the tag.
Process identifiers are often not unique -- a BPMN export numbers its streams and its material
states from "1" alike -- so on a process model use find_by_tag when a tag may repeat.
Filter before checking uniqueness to distinguish a loop from its instrument:
from pydexpi.query import QueryEngine
engine = QueryEngine(model)
instrument = engine.get_by_tag("4712.01", type="ProcessInstrumentationFunction")
loop = engine.get_by_tag("4712.01", type="InstrumentationLoopFunction")
instrument = engine.get_by_tag("PI-4712.01")
Multiple instruments can share a number, and even a printed tag. Such duplicates remain
ambiguous; no instrument or loop is preferred implicitly. The resolver also accepts printed
tags, preserving its existing precedence of object IDs over tags. MCP's find_by_tag and
get_by_tag tools accept the same type filter and aliases.
find_all(type=None, has_graphic=None)¶
Find all objects in the model, optionally filtered by type name and/or whether they have a graphical representation.
# All objects
everything = model.find_all()
# Filter by type (matches class name and parent classes)
pumps = model.find_all(type="Pump") # includes CentrifugalPump, ReciprocatingPump, etc.
tanks = model.find_all(type="Tank")
# Filter by graphic presence
with_graphics = model.find_all(has_graphic=True)
# Combined filters
graphic_pumps = model.find_all(type="Pump", has_graphic=True)
Connectivity Queries¶
neighbors(ref)¶
Get the immediate graph neighbors of an object. The ref parameter accepts an object instance, an ID string, or a tag string.
result = model.neighbors("P-101")
print(result.source) # the pump object
print(result.neighbors) # list of connected objects
find_connections(ref)¶
Alias for neighbors().
find_paths(source_ref, target_ref, max_depth=10, max_paths=10)¶
Find paths between two objects in the process graph. Parameters are bounded to avoid expensive traversals.
result = model.find_paths("TK-101", "P-201", max_depth=8)
print(len(result)) # number of paths found
for path in result.paths:
print([obj.id for obj in path])
Inspection¶
describe(ref)¶
Describe one object: its type and tag, the data attributes that carry a value, its composition owners (nearest first), the piping segment and system it belongs to, and its own piping nodes with whether each is connected.
d = q.describe("GlobeValve1")
print(d.dexpi_type, d.segment.id, d.system.id) # GlobeValve PipingNetworkSegment19 PipingNetworkSystem7
print(d.tag_name, d.display_name) # the tag, and the caption get_display_name gives
print([type(o).__name__ for o in d.owners]) # ['PipingNetworkSegment', 'PipingNetworkSystem', 'PlantModel']
print([(n.node.id, n.connected) for n in d.nodes]) # [('PipingNode51', True), ('PipingNode52', True)]
print(d.attributes) # data attributes; None and empty values left out
A node counts as connected when a piping connection starts or ends at it, or a segment starts or ends at it from outside the segment. A segment that starts at one of its own items (an off-page connector, say) names that item's node without connecting anything to it.
free_ports(ref)¶
The piping nodes of an object that nothing connects to yet, each with the item that owns it. For equipment the ports are its nozzles' nodes; for anything with nodes of its own (a nozzle, a valve, an off-page connector) they are those nodes.
Both are cached with the index; call invalidate() after mutating the model.
Sheet / Layout Queries¶
find_on_sheet(sheet_name)¶
Find all conceptual objects represented on a named diagram sheet.
find_graphics_for(ref)¶
Get the graphical representations (RepresentationGroups) for a conceptual object.
find_semantic_for_graphic(graphic_id)¶
Reverse lookup: given a graphic representation ID, find the conceptual object it represents.
Cache Management¶
model.q.invalidate()¶
Clear all cached indexes and graphs. Call this after mutating the model to ensure queries reflect the current state.
Architecture¶
Module Structure¶
pydexpi/query/
├── __init__.py # Public API exports
├── engine.py # QueryEngine — main public interface
├── index.py # ModelIndex — cached lookup indexes
├── resolver.py # Resolver — flexible reference resolution
├── results.py # Typed result objects (ConnectionResult, PathResult, etc.)
└── errors.py # Custom exceptions (ObjectNotFoundError, AmbiguousTagError)
Design Principles¶
-
Layer above, not replacement: The Query API reuses existing
model_toolkit.get_all_instances_in_model()for traversal andGraphLoader.dexpi_to_graph()for NetworkX graph construction. -
Lazy initialization: Indexes and the graph are built on first access and cached. This avoids unnecessary work if only a subset of query methods is used.
-
Explicit ambiguity:
get_*methods raise clear exceptions when results are ambiguous.find_*methods return lists, making multiplicity visible. -
Flexible references: Most methods accept an object instance, ID string, or tag string. The resolver tries ID lookup first, then tag lookup, preferring exact matches.
-
Bounded queries: Path queries support
max_depthandmax_pathsto prevent expensive traversals on large models.
Reuse of Existing Logic¶
| Query API component | Existing code reused |
|---|---|
| Instance traversal | model_toolkit.get_all_instances_in_model() |
| Graph construction | GraphLoader.dexpi_to_graph() |
| Path finding | networkx.all_simple_paths() on undirected view |
| Tag access | display_name.get_tag() |
| Graphics mapping | Diagram / RepresentationGroup tree walking |
Custom Exceptions¶
QueryError— base class for all query errorsObjectNotFoundError— raised when a lookup yields no resultsAmbiguousTagError— raised whenget_by_tag()finds multiple objects with the same tag
Result Types¶
QueryResult— iterable list wrapper with.first,.ids,len(), andbool()supportConnectionResult— wraps a source object and its list of neighborsPathResult— wraps source, target, and a list of paths (each path is a list of objects)ObjectDescription— the result ofdescribe(): type, tag, display name, attributes, owners, segment, system andNodeStatesNodeState— a piping node and whether it is connectedFreePort— an unconnected piping node and the item that owns it