Introduction
PIComposer is a compute-centric BIM/PMI EXPRESS data management system that implements a sophisticated hybrid object model. To use it effectively, understanding its core modeling concepts and the available programming interfaces is essential.
This article explores PIComposer’s unique approach to object modeling, its buffer management system, and the three complementary APIs it provides for data manipulation.
1. Traditional Approaches: Reference vs. Composition
Before diving into PIComposer’s model, let’s understand the two traditional approaches to object modeling in the context of ISO 10303 (STEP) data.
1.1. Object Reference Model (Traditional ISO 10303)
In standard exchange formats like Part 21 physical files, data is typically represented with references:
#1=IFCPERSON($,' ','[email protected]',$,$,$,$,$);
#2=IFCORGANIZATION($,'Spatial Compute',$,$,$);
#4=IFCPERSONANDORGANIZATION(#1,#2,$);
Translated into an object model, this becomes:
struct IfcPerson
{
std::string FamilyName;
// ...
};
struct IfcOrganization
{
std::string Name;
// ...
};
struct IfcPersonAndOrganization
{
std::shared_ptr<IfcPerson> ThePerson;
std::shared_ptr<IfcOrganization> TheOrganization;
// ...
};
Here, IfcPersonAndOrganization shares and reuses the IfcPerson and IfcOrganization instances with other instances within the model.
Advantages: - Maximum space efficiency through object sharing - Natural representation of complex relationships
Disadvantages: - Complex object lifecycle management - Intricate dependency tracking - Challenging garbage collection
1.2. Object Composition Model
Alternatively, objects can be composed by embedding subobjects directly:
struct IfcPersonAndOrganization
{
IfcPerson person; // Embedded, not referenced
IfcOrganization organization; // Embedded, not referenced
// ...
};
Advantages: - Simple lifecycle management (parent owns children) - Memory locality for better performance - No dependency tracking overhead
Disadvantages: - No sharing of common subobjects - Data duplication
PIComposer’s Hybrid Model
PIComposer implements a hybrid object model that combines the best of both worlds: composition whenever possible, and references only when necessary. This provides a powerful and flexible foundation for managing complex EXPRESS data.
1. The Hybrid Approach
The fundamental rule of ISO 10303 Part 21 (and the mostly unused Part 28) standard states that attributes of an ENTITY type must be encoded as references in exchange formats. This prevents data duplication and enables complex data sharing. PIComposer extends this paradigm with a very expressive, polymorphic object model, offering five distinct instance types:
-
Objectified References: An
IInstanceof a reference type is an objectified reference. Its sole purpose is to provide a reference to another instance. These lightweight instances allow PIComposer to faithfully represent the standard’s reference-based model. -
Fully Populated Instances: An
IInstancecan also be a complete, composition-contained object. This is a PIComposer enhancement for optimized performance, where all data is stored directly within the instance itself. -
Hybrid Instances: An instance where some of its attributes are shared instances, while others are fully populated. This offers a flexible middle ground, allowing sharing where beneficial while maintaining composition where performance is critical.
-
Internal Instance Sharing: An
IInstancemay contain specialIInstancesub-objects called composites that are shared within the instance’s own composition. This allows for efficient sharing of sub-structures within a single instance. -
Inverse Instances: A special reference instance to manage referential integrity. It provides a reverse link to the instance that referenced the current instance, enabling bidirectional traversal and relationship management.
PIComposer provides primitives in its API and special commands to leverage these different mechanisms. This hybrid model allows developers to choose the most appropriate representation for their use case, whether it’s maximizing data sharing, optimizing for performance, or faithfully representing the original EXPRESS schema.
2. Buffer Management and Memory Model
A key innovation in PIComposer is its sophisticated buffer-based memory model.
2.1. Buffer Ownership Hierarchy
Two EXPRESS data types—ENTITY and SELECT—maintain buffers that store their underlying data in a hierarchical relationship:
-
Master Buffer: The top-level instance in a composition owns the master buffer containing all nested attribute data in a contiguous memory region.
-
Child Buffers: Inner Entity and Select instances share their parent’s master buffer, acting as views into specific regions of that buffer.
2.2. Detaching from the Master Buffer
Inner instances can gain independent buffer ownership through detachment:
// Get a profile from an extruded area solid (initially shares buffer)
final profile = extrude.getInstance("SweptArea");
// Detach BEFORE modifying the parent buffer
profile.detach(); // profile now has independent buffer
// Modify parent buffer - profile remains unchanged
extrude.setAttributesByJson({
"ExtrudedDirection": {
"@type": "IfcDirection",
"DirectionRatios": [0.0, 0.0, 1.0]
}
});
// profile.getRunTimeType() now returns InstanceRunTimeType.free
When detached, the instance transitions from InstanceRunTimeType.attached to InstanceRunTimeType.free, gaining its own private data copy.
2.3. When to Detach
Detach an instance when:
- You need to modify a child instance independently of its parent.
- The parent buffer will undergo changes (e.g., via setAttributesByJson()).
- You want to extract a subgraph for independent processing.
- You need to ensure data isolation between operations.
2.4. In-Place Mutation Optimization
For fixed-size primitive types whose mutation doesn’t change buffer size, updates occur in situ (directly in the original buffer):
-
Primitive types: INTEGER, REAL, BOOLEAN, ENUM
-
Fixed-size aggregates: Arrays with fixed dimensions
Key implication: When an inner attribute instance mutates a fixed-size primitive value, it modifies the master buffer directly. Changes are immediately visible to the parent and any other views into the same buffer region.
Three Complementary APIs
PIComposer provides three powerful and complementary APIs for working with instance data.
1. 1. Fine-Grained Imperative API
Type-safe methods for precise attribute manipulation:
// Get values with type safety
final x = point.getReal(attName: 'Coordinates', index: 0);
final name = wall.getString(attName: 'Name');
// Set values directly
point.setAttribute<double>(10.5, attName: 'Coordinates', index: 0);
wall.setAttribute<String>('Main Wall', attName: 'Name');
2. 2. Declarative JSON API
Batch operations using JSON for complex hierarchies with special keys:
Reserved Keys:
- @type: Type specification for an object.
- @reference: Instruction to treat object as a reference.
Example: Creating an IfcLocalPlacement
final placement = model.instanceFromJson({
"@type": "IfcLocalPlacement",
"PlacementRelTo": {
"@reference": parentPlacement
},
"RelativePlacement": {
// Select type: IfcAxis2Placement3D
"IfcAxis2Placement3D": {
"@type": "IfcAxis2Placement3D",
"Location": {
"@type": "IfcCartesianPoint",
"Coordinates": [0, 0, 0]
},
"Axis": {
"@type": "IfcDirection",
"DirectionRatios": [0, 0, 1]
},
"RefDirection": {
"@type": "IfcDirection",
"DirectionRatios": [1, 0, 0]
}
}
}
});
The @reference key instructs the model to:
1. Create a reference to the parentPlacement instance.
2. Add an inverse reference from parentPlacement back to the new placement.
Inverses are object references stored as persistent InstanceHandle objects, used for:
- Instance traversal (finding children from parents).
- Lifecycle management (preventing deletion of referenced instances).
Example: Updating with JSON
final axis2Placement = placement.getInstance(attName: 'RelativePlacement');
axis2Placement.setAttributesByJson({
"Location": {
"@type": "IfcCartesianPoint",
"Coordinates": [100, 0, 0]
}
});
Since IfcLocalPlacement Location attribute is of IfcCartainPoint type, which is final, this could be simplify to:
final axis2Placement = placement.getInstance(attName: 'RelativePlacement');
axis2Placement.setAttributesByJson({
"Location": {
"Coordinates": [100, 0, 0]
}
});
Removing the @type annotation.
3. 3. Path-Based Universal API
A result of PIComposer’s sophisticated hybrid model is that an instance possessing a deeply nested structure makes traditional data access traversal tedious and error-prone. PIComposer introduces a simple and innovative solution: the EXPRESS path API. Any attribute within an instance can be retrieved or updated by a single path-based API, no matter the complex structural complexity.
It is simple because it does not invent new syntax or use special techniques like XPath or JSONPath. A path is simply an array of numbers or strings, a familiar and often-used artifact in any programming language.
The path API provides a unified way to access data anywhere in the instance hierarchy using a list of tokens (strings for attributes, integers for indices).
It is universal because it is portable across all models of the same schema. An attribute path is comprised of an ordered array of an instance’s attribute names, a select’s selected value type, and indices into attribute collections. Most notably, it does not contain any instance id. This design ensures that paths are schema-driven rather than instance-driven, making them reusable across different models and datasets.
A path is considered valid in a given model if any subpath addresses a valid value in the model, including crossing instance boundaries. This means paths can traverse from one instance to another through references, following the object graph naturally.
Path Definition:
typedef PIAttributePath = List<dynamic>;
// String tokens: instance attribute names or select selected value type
// Integer tokens: indices into aggregates
Example: Accessing IfcProduct Placement Coordinates x value (or first coordinate)
final path = [
'ObjectPlacement', // Attribute → IfcLocalPlacement
'RelativePlacement', // Attribute → IfcAxis2Placement select
'IfcAxis2Placement3D', // Select value type → IfcAxis2Placement3D
'Location', // Attribute → IfcCartesianPoint
'Coordinates', // Attribute → LIST OF REAL
0 // Index → First coordinate (x)
];
Using the Path API:
// Get value with type information
final (type, value) = beam.getAttributeByPath(path);
// Set value directly
final updated = beam.setAttributeByPath(path, 2000);
model.saveInstance(updated);
// Remove from aggregate
beam.removeAttributeFromAggregateByPath(path);
// Nullify optional attribute
beam.nullifyAttributeByPath(path);
// Clear entire aggregate
beam.clearAttributeAggregateByPath(path);
Path API with Detach Control:
// Get value and detach it for independent manipulation
final (type, value) = beam.getAttributeByPath(path, detach: true);
// 'value' now has its own buffer and can be modified independently
3.1. Traversing Instance Boundaries
PIComposer’s path traversal implementation crosses instance persistence boundaries, as long as the referenced instances are persisted in the database. This powerful capability enables operations across related instances without manual dereferencing.
Example: Updating Related Instances Through a Relationship
Consider an IfcRelContainedInSpatialStructure relationship that contains a list of related products within a building storey:
// Path to access all products in a spatial structure
final relContained = model.getInstance(relcontainedInspatialHandle);
for(int i = 0; i < RelatedElementsCount; ++i) {
final locationPath = ['RelatedElements', i, ObjectPlacement', 'RelativePlacement',
'IfcAxis2Placement3D', 'Location', 'Coordinates'];
final product = relContained.setAttributeByPath(locationPath, [newX, newY, newZ]);
model.saveInstance(product);
}
A single entry point allows PIComposer to update every product in a buiding sotrey.
This cross-boundary traversal capability is particularly powerful for:
-
Bulk updates: Modifying properties across many related instances
-
Validation: Checking consistency across instance boundaries
-
Analysis: Gathering data from distributed object graphs
-
Migration: Applying schema changes across related entities
3.2. Performance Characteristics
Operation |
Performance |
Notes |
Path resolution |
O(depth) |
Cached for repeated access |
Attribute access |
O(1) after resolution |
Direct memory access |
Cross-boundary traversal |
O(depth + references) |
Depends on graph depth |
Detach in path |
O(size) |
Copies data to new buffer |
3.3. Relationship to Other APIs
The Path API complements the Imperative and JSON APIs:
// Imperative: Type-safe, explicit
final x = point.getReal(attName: 'Coordinates', index: 0);
// Path: Dynamic, flexible, cross-boundary
final x = point.getAttributeByPathAsDynamic(['Coordinates', 0]);
final (funType, x) = point.getAttributeByPath(['Coordinates', 0]);
// JSON: Batch, hierarchical
point.setAttributesByJson({'Coordinates': [10.5, 0, 0]});
// Path: Targeted, efficient, cross-instance
point.setAttributeByPath(['Coordinates', 0], 10.5);
Choose the appropriate API based on your needs: - Path API: Cross-boundary operations, runtime-dynamic access, deep hierarchies, scripting - Imperative API: Compile-time safety, simple operations, IDE autocomplete - JSON API: Complex creation, batch updates, human-readable configuration
Model-Level Operations
The IModel interface provides comprehensive model management capabilities.
1. Instance Lifecycle
// Create and save instances
final wall = model.createInstance(typeName: 'IfcWall');
model.saveInstance(wall, tag: 'structural');
// Batch operations
model.saveInstances([wall1, wall2, wall3]);
// Check before deletion, instance should not be deleted if
// there is inverse pointing to it.
if (model.canDelete(placement)) {
model.deleteInstance(placement);
}
2. Querying Instances
// Get by type (with subtype inclusion)
final walls = model.getInstancesByType(
typeName: 'IfcWall',
includeSubType: true
);
// Paginated for large models
InstanceHandle cursor = InstanceHandle.nullHandle();
do {
final page = model.getInstancesPaginated(cursor, pageSize: 500);
if (page.isEmpty) break;
processPage(page);
cursor = page.last.instanceHandle;
} while (true);
// Tag-based retrieval
model.tagInstance(wall, 'exterior');
final exteriorWalls = model.getInstancesByTag('exterior');
3. Cross-Model Operations
// Copy simple instance
final copied = model.copyInstance(sourceModel, sourceInstance);
// Copy with full composition hierarchy
final copies = model.copyInstanceWithComposedDependency(
sourceModel,
sourceInstance
);
// Resolve IDs for consistency
model.resolveIndices(copiedInstances);
4. Path Operations at Model Level
The model also provides path-based operations for working across instance boundaries:
final path = InstancePath(wallHandle, [
'ObjectPlacement',
'RelativePlacement',
'IfcAxis2Placement3D',
'Location'
]);
// Get value with type
final (type, value) = model.getAttributeByPath(path);
// Set value
model.setAttributeByPath(path, newLocation);
// Set with JSON
model.setAttributeByPathWithJson(path, {
"@type": "IfcCartesianPoint",
"Coordinates": [10, 20, 30]
});
SELECT Type Management
The ISelect interface manages EXPRESS SELECT types (unions). Each select either access its parent buffer or when detached own its buffer to store its data.
1. SELECT States
A SELECT can be in one of three states:
-
Determined: Specific type selected with stored value.
-
Indetermined: No type selected (invalid for operations). Select in this state is considered null in an aggregate of selects.
-
Null: Created by
createNullSelect().
2. Working with SELECTs
// Create a select
final measureSelect = instance.createSelect(attName: 'MeasureValue');
// Set the selected type
measureSelect.setSelectedType(typeName: 'IfcLengthMeasure');
// Set value
measureSelect.setValue<double>(10.5);
//select value's fundamental type property
final fun = measureSelect.selectedFundamentalType;
// Get value with detach control
final value = measureSelect.getSelectedValue<double>(detach: true);
// For entity selects
final entitySelect = instance.createSelect(attName: 'Placement');
entitySelect.setSelectedType(typeName: 'IfcAxis2Placement3D');
final placement = entitySelect.getInstance(detach: true);
// JSON representation
final json = measureSelect.toJson();
// Output: {"IfcLengthMeasure": 10.5}
3. SELECT Detachment
Like instances, SELECTs can be detached from parent buffers:
// Get a select (initially shares buffer)
final select = instance.getSelect(attName: 'Placement', detach: false);
// Detach for independent modification
select.detach();
// Now safe to modify parent
instance.setAttributesByJson({...});
Best Practices and Guidelines
1. Buffer Management
-
Detach before parent modification when you need child independence.
-
Use
detach: truein getters when you know you’ll need independent instances. -
Be aware of in-place mutations for fixed-size primitives.
-
Check
isDetachto understand buffer ownership.
2. API Selection
Scenario |
Recommended API |
Reason |
Deep attribute access |
Path API |
Most efficient, direct |
Complex creation |
JSON API |
Declarative, handles hierarchies |
Simple type-safe ops |
Imperative API |
Clear intent, compile-time checks |
Bulk operations |
Batch methods |
Performance |
Cross-model copying |
Model copy methods |
Handles dependencies |
3. Performance Optimization
-
Use paginated queries for large result sets.
-
Prefer batch operations over individual saves.
-
Use path API for frequent deep attribute access.
-
Consider detachment costs when designing workflows.
-
Monitor buffer sharing to avoid unintended mutations.
Conclusion
PIComposer’s hybrid object model combines the flexibility of references with the performance of composition, all managed through an innovative buffer system. The three complementary APIs provide developers with choice:
-
Imperative API for type-safe, precise control.
-
JSON API for declarative, hierarchical operations.
-
Path API for universal, efficient data access.
Understanding buffer management—particularly detachment and in-place mutation—is key to building efficient applications. Whether you’re working with deep geometric hierarchies, complex relationships, or large-scale models, PIComposer’s object model provides the tools you need.
The combination of these features makes PIComposer ideal for: - Importing/exporting complex BIM data. - Rapid prototyping of model structures. - Scripting complex model operations. - Building high-performance geometric applications. - Managing large-scale ISO 10303 data.