Area-Level Aggregation in AVEVA System Platform

I would like to inquire about the possibility of obtaining aggregated totals at the Area level within AVEVA System Platform.

Specifically, I am looking for a method to calculate and display:

  1. The total number of pumps currently running within a building, with further breakdowns at each level and floor.

  2. The total number of fault alerts at each Area level for all underlying devices.

I previously implemented dynamic binding using the BindTo method to achieve this aggregation. However, when applied to thousands of objects and attributes, the MX Service crashed due to the large number of calls.

Could you please advise whether there is a recommended or best-practice approach (e.g., roll-up attributes, scripts, queries, or templates) to perform hierarchical aggregation efficiently without impacting MX Service performance?

Parents
  • Hi Abid,

    I do actually have an answer for you that I have used two scripts which were pretty performant for my own use cases. I will try to find a spot to share the full example galaxy, but will post the two scripts here, hopefully they are understandable on their own.

    Script 1 — Find my children

    In SP 2026, this script will no longer be necessary as we will have a "children" array pre-pouplated for each object):

    Important: The scripts assume the model is areas in areas but with a slight modification it should work for objects in objects (In 2026 we hope to remove the need for distinguishing between these) 

    • Copies MyEngine.Engine.Objects[] into a local array (EngineObjects[]) for faster looping (performance best practice).
    • Loops through each object name.
      • Uses an indirect variable and BindTo() to dynamically read each object’s .Area (or .Container) attribute at runtime.
      • Compares that area (or container) to Me.Tagname.
      • If they match, then this is one of my children so add the matching object name into Me.MyObjects[].

    Execute:

    'This is the simplest implementation - is directly builds the Attribute array (no log messages)
    dim StartTime as System.DateTime;						'Script start time (for diagnostics)
    dim EndTime as System.DateTime;							'Script end time (for diagnostics)
    dim Duration as System.TimeSpan;						'Script duration (for diagnostics)
    dim EngineObjects[1] as string;							'Define a local array for speed (it is faster than using attribute array directly)
    dim TheObject as string;								'Represents an object's name
    dim TheArea as indirect;								'The object's container
    dim Count as integer;									'Count of objects
    StartTime = now();										'Script start time (for diagnostics)
    Count = 0;												'Initialise to 0
    EngineObjects[] = MyEngine.Engine.Objects[];			'Grab the attrribute and put it in the local (faster)
    
    for each TheObject in EngineObjects[]					'Iterate through them
    	TheArea.BindTo(TheObject+".Area");					'Look at each object's area (this will work for .Container too
    	if TheArea == Me.Tagname then						'If it is me then
    		Count = Count + 1;								'Add 1 to the count
    		me.MyObjects[].Dimension1 = Count;				'Resize the target array
    		me.MyObjects[Count] = TheObject;				'Put the object's name in the new element of the array
    	endif;												'TheArea == Me.Tagname
    next;													'TheObject in EngineObjects[]
    
    'Compute duration
    EndTime = now();										'Script end time (for diagnostics)
    Duration = EndTime - StartTime;							'Script duration (for diagnostics)
    Me.MyObjects.Duration = Duration.TotalMilliseconds;		'Store the duration to historise it and trend it (for diagnostics)
    
    Me.MyObjects.Initialised = true;						'Don't run again

    Script 2 — Run through my children's status and log it

    Global indirect arrays are declared to hold live bindings:

    • Status[] binds to each child object’s .Status — this can be Stopped, Backwash, Rinse, Running, or Maintenance
    • StatusArrays[] and StatusDims[] bind to this object’s 5 output arrays (one for each state above) and their .Dimension1 sizes.
    • Initialised tracks whether the BindTo() work has been done yet.

    High level it has 2 steps:

    1. Run once only: Bind a set of indirect arrays to the childrens' statuses
    2. Preiodically after #1: Run though the statuses and log them in arrays 

    Detail description: 

    • Safety check: if Status[].Length is smaller than Me.MyObjects[].Dimension1, it logs a warning and does nothing else (you need a bigger Status[] array).
    • First-time run-once setup (not Initialised):
      • Loops through Me.MyObjects[] and BindTo() each child object’s .Status into Status[ObjectIndex].
      • Loops StatusIndex = 1 to 5 and BindTo() this object’s five “bucket” arrays (like Me.MyObjects.01[] … Me.MyObjects.05[]) plus each bucket’s .Dimension1.
      • Sets Initialised = true so future scans only evaluate (no re-binding).
    • Normal scan (already initialised):
      • Creates StatusAL[5] (five System.Collections.ArrayList) to build the results in memory (faster than writing attribute arrays repeatedly).
      • Clears the 5 output arrays by setting each bound dimension (StatusDims[1..5]) to 0.
      • For each child object, reads its bound Status[ObjectIndex] value (1–5) and adds the object name into the matching ArrayList.
      • Transfers results back to attributes:
        • For each status 1..5, if its ArrayList exists, sets the corresponding output array size (StatusDims[StatusIndex]) and copies the ArrayList into the bound attribute array (StatusArrays[StatusIndex] = ...ToArray(...)).

    Global Declarations:

    dim Status[260] as indirect;		'Will keep the live values
    dim StatusArrays[5] as indirect;	'5 Arrays for the 5 states
    dim StatusDims[5] as indirect;		'Sizes of the 5 arrays for the 5 states
    dim Initialised as boolean;			'True once Bindto's are executed

    Script — While true: Me.MyObjects.Initialised (every scan):

    'This uses Arraylists for speed - it builds the aray in memory first.
    dim StartTime as System.DateTime;			'Script start time (for diagnostics)
    dim EndTime as System.DateTime;				'Script end time (for diagnostics)
    dim Duration as System.TimeSpan;			'Script duration (for diagnostics)
    dim ObjectIndex as integer;					'Index of objects in array
    dim StatusIndex as integer;					'Index of the Statuses
    StartTime = now();							'Script start time (for diagnostics)
    
    if Status[].Length < Me.MyObjects[].Dimension1 then								'Do not continue if the array is not large enough, log a warning to tell the user to increase the size
    	LogWarning("Status array only has space for "+Status[].Length+" items only - increase it to at least "+Me.MyObjects[].Dimension1);
    else	'not (Status[].Length < Me.MyObjects[].Dimension1)
    	if not Initialised then														'If Bindto's have not been executed
    	'---------------------------------------------------------------------------------------------------------------
    		'Bind to Statuses of each object
    		for ObjectIndex = 1 to Me.MyObjects[].Dimension1						'Run through every child object (built by Initialise script
    			Status[ObjectIndex].BindTo(Me.MyObjects[ObjectIndex]+".Status");	'Bind to the objects's status attribute
    		next;	'ObjectIndex
    
    		'Bind to the 5 status arrays of this object
    		for StatusIndex = 1 to 5 												'Run through each of the 5 statuses
    			dim ArrayID as string;												'String representing the array's name
    			ArrayID = "me.MyObjects."+Text(StatusIndex,"0#")+"[]";				'Populate string representing the array's name with leading 0
    			StatusArrays[StatusIndex].BindTo(ArrayID);							'Bind to each Status array
    			StatusDims[StatusIndex].BindTo(ArrayID+".Dimension1");				'Bind to each Status array's size
    		next;	'StatusIndex
    
    		Initialised = true;														'Binding complete, next time, just evaluate
    	'---------------------------------------------------------------------------------------------------------------
    	else	'Initialised	
    		'If the Bindto's are completed, just evaluate them																	
    		dim StatusAL[5] as System.Collections.ArrayList;						'Arraylists are faster than working directly with Attribute Arrays
    		dim ObjectName as string;												'String of Object's name
    		
    		'Clear the Status arrays
    		for StatusIndex = 1 to 5 												'For each status array
    			StatusDims[StatusIndex] = 0;										'Zero Status arrays' lengths to clear them 
    		next;																	'StatusIndex
    
    		'Assign Object to correct Status ArrayList
    		for ObjectIndex = 1 to Me.MyObjects[].Dimension1						'Check every object
    			ObjectName = Me.MyObjects[ObjectIndex];								'Get the Object's name
    
    			'Ensure Arraylist exists
    			if StatusAL[Status[ObjectIndex]] == null then									'If the Arraylist does not exist
    				StatusAL[Status[ObjectIndex]] = new System.Collections.ArrayList();			'Create a new the Array list
    			endif;	'StatusAL[Status[ObjectIndex]] == null
    			
    			'Add the Object to the Array list
    			if Status[ObjectIndex] > 0 or Status[ObjectIndex] <= 5 then						'Check for valid status index
    				StatusAL[Status[ObjectIndex]].Add(ObjectName);								'Add the object to the arraylist
    			else	'Not (Status[ObjectIndex] > 0 or Status[ObjectIndex] <= 5)															'Warn if not valid status
    				LogWarning("Invalid status value ("+Status[ObjectIndex]+") for Me.MyObjects["+ObjectIndex+"] = " + ObjectName);
    			endif;	'Status[ObjectIndex] > 0 or Status[ObjectIndex] <= 5 
    		next;	'ObjectIndex
    
    		'Transfer Arraylist to (slower) Attribute Arrays
    		for StatusIndex = 1 to 5																	'For each Status array
    			if StatusAL[StatusIndex] <> null then													'If it is not empt
    				StatusDims[StatusIndex] = StatusAL[StatusIndex].Count;								'Set array size
    				StatusArrays[StatusIndex] = StatusAL[StatusIndex].ToArray(ObjectName.GetType());	'Transfer array
    			endif;	'Not empty
    		next;	'StatusIndex
    	endif;	'Initialised
    endif;	'Status[].Length < Me.MyObjects[].Dimension1
    
    'Compute duration
    EndTime = now();										'Script end time (for diagnostics)
    Duration = EndTime - StartTime;							'Script duration (for diagnostics)
    Me.CheckStatus.Duration = Duration.TotalMilliseconds;	'Store the duration to historise it and trend it (for diagnostics)

     - Ernst

Reply
  • Hi Abid,

    I do actually have an answer for you that I have used two scripts which were pretty performant for my own use cases. I will try to find a spot to share the full example galaxy, but will post the two scripts here, hopefully they are understandable on their own.

    Script 1 — Find my children

    In SP 2026, this script will no longer be necessary as we will have a "children" array pre-pouplated for each object):

    Important: The scripts assume the model is areas in areas but with a slight modification it should work for objects in objects (In 2026 we hope to remove the need for distinguishing between these) 

    • Copies MyEngine.Engine.Objects[] into a local array (EngineObjects[]) for faster looping (performance best practice).
    • Loops through each object name.
      • Uses an indirect variable and BindTo() to dynamically read each object’s .Area (or .Container) attribute at runtime.
      • Compares that area (or container) to Me.Tagname.
      • If they match, then this is one of my children so add the matching object name into Me.MyObjects[].

    Execute:

    'This is the simplest implementation - is directly builds the Attribute array (no log messages)
    dim StartTime as System.DateTime;						'Script start time (for diagnostics)
    dim EndTime as System.DateTime;							'Script end time (for diagnostics)
    dim Duration as System.TimeSpan;						'Script duration (for diagnostics)
    dim EngineObjects[1] as string;							'Define a local array for speed (it is faster than using attribute array directly)
    dim TheObject as string;								'Represents an object's name
    dim TheArea as indirect;								'The object's container
    dim Count as integer;									'Count of objects
    StartTime = now();										'Script start time (for diagnostics)
    Count = 0;												'Initialise to 0
    EngineObjects[] = MyEngine.Engine.Objects[];			'Grab the attrribute and put it in the local (faster)
    
    for each TheObject in EngineObjects[]					'Iterate through them
    	TheArea.BindTo(TheObject+".Area");					'Look at each object's area (this will work for .Container too
    	if TheArea == Me.Tagname then						'If it is me then
    		Count = Count + 1;								'Add 1 to the count
    		me.MyObjects[].Dimension1 = Count;				'Resize the target array
    		me.MyObjects[Count] = TheObject;				'Put the object's name in the new element of the array
    	endif;												'TheArea == Me.Tagname
    next;													'TheObject in EngineObjects[]
    
    'Compute duration
    EndTime = now();										'Script end time (for diagnostics)
    Duration = EndTime - StartTime;							'Script duration (for diagnostics)
    Me.MyObjects.Duration = Duration.TotalMilliseconds;		'Store the duration to historise it and trend it (for diagnostics)
    
    Me.MyObjects.Initialised = true;						'Don't run again

    Script 2 — Run through my children's status and log it

    Global indirect arrays are declared to hold live bindings:

    • Status[] binds to each child object’s .Status — this can be Stopped, Backwash, Rinse, Running, or Maintenance
    • StatusArrays[] and StatusDims[] bind to this object’s 5 output arrays (one for each state above) and their .Dimension1 sizes.
    • Initialised tracks whether the BindTo() work has been done yet.

    High level it has 2 steps:

    1. Run once only: Bind a set of indirect arrays to the childrens' statuses
    2. Preiodically after #1: Run though the statuses and log them in arrays 

    Detail description: 

    • Safety check: if Status[].Length is smaller than Me.MyObjects[].Dimension1, it logs a warning and does nothing else (you need a bigger Status[] array).
    • First-time run-once setup (not Initialised):
      • Loops through Me.MyObjects[] and BindTo() each child object’s .Status into Status[ObjectIndex].
      • Loops StatusIndex = 1 to 5 and BindTo() this object’s five “bucket” arrays (like Me.MyObjects.01[] … Me.MyObjects.05[]) plus each bucket’s .Dimension1.
      • Sets Initialised = true so future scans only evaluate (no re-binding).
    • Normal scan (already initialised):
      • Creates StatusAL[5] (five System.Collections.ArrayList) to build the results in memory (faster than writing attribute arrays repeatedly).
      • Clears the 5 output arrays by setting each bound dimension (StatusDims[1..5]) to 0.
      • For each child object, reads its bound Status[ObjectIndex] value (1–5) and adds the object name into the matching ArrayList.
      • Transfers results back to attributes:
        • For each status 1..5, if its ArrayList exists, sets the corresponding output array size (StatusDims[StatusIndex]) and copies the ArrayList into the bound attribute array (StatusArrays[StatusIndex] = ...ToArray(...)).

    Global Declarations:

    dim Status[260] as indirect;		'Will keep the live values
    dim StatusArrays[5] as indirect;	'5 Arrays for the 5 states
    dim StatusDims[5] as indirect;		'Sizes of the 5 arrays for the 5 states
    dim Initialised as boolean;			'True once Bindto's are executed

    Script — While true: Me.MyObjects.Initialised (every scan):

    'This uses Arraylists for speed - it builds the aray in memory first.
    dim StartTime as System.DateTime;			'Script start time (for diagnostics)
    dim EndTime as System.DateTime;				'Script end time (for diagnostics)
    dim Duration as System.TimeSpan;			'Script duration (for diagnostics)
    dim ObjectIndex as integer;					'Index of objects in array
    dim StatusIndex as integer;					'Index of the Statuses
    StartTime = now();							'Script start time (for diagnostics)
    
    if Status[].Length < Me.MyObjects[].Dimension1 then								'Do not continue if the array is not large enough, log a warning to tell the user to increase the size
    	LogWarning("Status array only has space for "+Status[].Length+" items only - increase it to at least "+Me.MyObjects[].Dimension1);
    else	'not (Status[].Length < Me.MyObjects[].Dimension1)
    	if not Initialised then														'If Bindto's have not been executed
    	'---------------------------------------------------------------------------------------------------------------
    		'Bind to Statuses of each object
    		for ObjectIndex = 1 to Me.MyObjects[].Dimension1						'Run through every child object (built by Initialise script
    			Status[ObjectIndex].BindTo(Me.MyObjects[ObjectIndex]+".Status");	'Bind to the objects's status attribute
    		next;	'ObjectIndex
    
    		'Bind to the 5 status arrays of this object
    		for StatusIndex = 1 to 5 												'Run through each of the 5 statuses
    			dim ArrayID as string;												'String representing the array's name
    			ArrayID = "me.MyObjects."+Text(StatusIndex,"0#")+"[]";				'Populate string representing the array's name with leading 0
    			StatusArrays[StatusIndex].BindTo(ArrayID);							'Bind to each Status array
    			StatusDims[StatusIndex].BindTo(ArrayID+".Dimension1");				'Bind to each Status array's size
    		next;	'StatusIndex
    
    		Initialised = true;														'Binding complete, next time, just evaluate
    	'---------------------------------------------------------------------------------------------------------------
    	else	'Initialised	
    		'If the Bindto's are completed, just evaluate them																	
    		dim StatusAL[5] as System.Collections.ArrayList;						'Arraylists are faster than working directly with Attribute Arrays
    		dim ObjectName as string;												'String of Object's name
    		
    		'Clear the Status arrays
    		for StatusIndex = 1 to 5 												'For each status array
    			StatusDims[StatusIndex] = 0;										'Zero Status arrays' lengths to clear them 
    		next;																	'StatusIndex
    
    		'Assign Object to correct Status ArrayList
    		for ObjectIndex = 1 to Me.MyObjects[].Dimension1						'Check every object
    			ObjectName = Me.MyObjects[ObjectIndex];								'Get the Object's name
    
    			'Ensure Arraylist exists
    			if StatusAL[Status[ObjectIndex]] == null then									'If the Arraylist does not exist
    				StatusAL[Status[ObjectIndex]] = new System.Collections.ArrayList();			'Create a new the Array list
    			endif;	'StatusAL[Status[ObjectIndex]] == null
    			
    			'Add the Object to the Array list
    			if Status[ObjectIndex] > 0 or Status[ObjectIndex] <= 5 then						'Check for valid status index
    				StatusAL[Status[ObjectIndex]].Add(ObjectName);								'Add the object to the arraylist
    			else	'Not (Status[ObjectIndex] > 0 or Status[ObjectIndex] <= 5)															'Warn if not valid status
    				LogWarning("Invalid status value ("+Status[ObjectIndex]+") for Me.MyObjects["+ObjectIndex+"] = " + ObjectName);
    			endif;	'Status[ObjectIndex] > 0 or Status[ObjectIndex] <= 5 
    		next;	'ObjectIndex
    
    		'Transfer Arraylist to (slower) Attribute Arrays
    		for StatusIndex = 1 to 5																	'For each Status array
    			if StatusAL[StatusIndex] <> null then													'If it is not empt
    				StatusDims[StatusIndex] = StatusAL[StatusIndex].Count;								'Set array size
    				StatusArrays[StatusIndex] = StatusAL[StatusIndex].ToArray(ObjectName.GetType());	'Transfer array
    			endif;	'Not empty
    		next;	'StatusIndex
    	endif;	'Initialised
    endif;	'Status[].Length < Me.MyObjects[].Dimension1
    
    'Compute duration
    EndTime = now();										'Script end time (for diagnostics)
    Duration = EndTime - StartTime;							'Script duration (for diagnostics)
    Me.CheckStatus.Duration = Duration.TotalMilliseconds;	'Store the duration to historise it and trend it (for diagnostics)

     - Ernst

Children
No Data