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;
  // ...
};

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, references only when necessary.

1. The Hybrid Approach

The fundamental rule of ISO 10303 states that attributes of an ENTITY type must be encoded as references in exchange formats. This prevents data duplication and enables complex data sharing. However, PIComposer extends this paradigm for in-memory processing:

  1. Objectified References: IInstance can act as a lightweight reference handle (InstanceHandle), faithfully representing the standard’s model.

  2. Fully Populated Instances: IInstance can also be a complete, composition-contained object—a PIComposer enhancement for optimized performance.

This dual nature is managed through: - Completeness: An instance with no external references is "complete" - Composition: Attributes that are not references form atomic, internally consistent data units

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 typically share their parent’s master buffer, acting as views into specific regions

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

  • Fixed-length strings/binaries: When defined with fixed size in schema

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 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 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]
  }
});

3. 3. Path-Based Universal API

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

Path Definition:

typedef PIAttributePath = List<dynamic>;
// String tokens: attribute names or select type choices
// Integer tokens: indices into aggregates

Example: Accessing IfcBeam Placement Coordinates

final path = [
  'ObjectPlacement',           // Attribute → IfcLocalPlacement
  'RelativePlacement',         // Attribute → IfcAxis2Placement select
  'IfcAxis2Placement3D',       // Select 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. Performance Considerations

  • Path API: Most efficient for accessing composed attributes due to memory locality

  • JSON API: Convenient but has parsing overhead (JSON encode/decode between Dart and C++)

  • Imperative API: Best for simple operations and when type safety is paramount

Guidelines: - Use Path API for deep attribute access and updates - Use JSON API for complex hierarchical creation and batch operations - Use Imperative API for simple, type-safe operations - For large binary or numeric data, prefer Path or Imperative APIs over JSON

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
if (model.canDelete(wall)) {
  model.deleteInstance(wall);
}

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

1. SELECT States

A SELECT can be in one of three states: 1. Determined: Specific type selected with stored value 2. Indetermined: No type selected (invalid for operations) 3. 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);

// 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

  1. Detach before parent modification when you need child independence

  2. Use detach: true in getters when you know you’ll need independent instances

  3. Be aware of in-place mutations for fixed-size primitives

  4. Check isDetach to 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:

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