Hey everyone,
I am trying to create a rolling average of an analog data point for the last hour. I want to store 1 data point every 60 seconds ( as an example), and then take the sum for the hour to get the rolling average. I have a block that was created by our local business partner years ago, but it only shifts a new data point into an array every scan. I tried changing the blocks scan rate to see if that would work, but I don't think it is working correctly. When changing a function block scan rate multiplier, does this go by the module execution time, or the module scan rate selected in the properties (example, 1 second)? This module execution time is around 3000 micro seconds.
Here is the current composite calc expression:
(* Code for AVG_CALC calc block in AVERAGE composite template *) OPTION EXPLICIT; VAR INPUT; NUM_VALS; ENABLE; AVERAGE; COUNT; SUM; STATUS; SCANS; END_VAR; (* This routine wil calculate a rolling average. The value to be averaged is INPUT. The number of values to average is NUM_VALS. The maximum value of NUM_VALS is 100. *) (* Initialize inputs *) INPUT := IN1; NUM_VALS := IN2; ENABLE := IN3; (* If ENABLE is TRUE then place a new value in the stack in scan and shift existing values *) IF ENABLE = TRUE THEN SCANS := SCANS + 1; COUNT := 1; WHILE COUNT < NUM_VALS DO '^/STACK'[COUNT][1] := '^/STACK'[COUNT + 1][1]; COUNT := COUNT + 1; END_WHILE; '^/STACK'[NUM_VALS][1] := INPUT; (* Sum NUM_VALS in stack *) COUNT := 0; SUM := 0; WHILE COUNT <= NUM_VALS DO COUNT := COUNT + 1; SUM := SUM + '^/STACK'[COUNT][1] ; END_WHILE; AVERAGE := SUM / NUM_VALS; ELSE AVERAGE := INPUT; SCANS := 0; ENDIF; (* Set status to FALSE if average stack is not full *) STATUS := TRUE; IF SCANS < NUM_VALS THEN STATUS := FALSE; ENDIF; (* Set block outputs *) OUT1 := AVERAGE; OUT2 := SUM; OUT3 := SCANS; OUT4 := STATUS;
Thank you!
Jay
To use your current code, you would want to add a bit more logic. Normally it's based on the sample rate of the module, however you could also use a few blocks to get it to execute every 60 seconds and store 60 samples to get your hour average. For example, rework this code into an action block and use an OND wired to a NOT block wired back to itself. Then also wire the OND output to the action block. This will basically create a pulse every 60 seconds.
To comment on that calc block expression, it's not a very efficient one and with a few tweaks should work fine for what you are doing. The things that are inefficient is looping and summing every time it runs. It's better to keep a point to a position in the array, increment it each scan and then subtract the old value and add the new value each time.
This here is a calc expression (with a picture to show the ins and outs) I have made which is an alternative as well in that it will use even more samples to get your average. It samples every scan rate and then combines a small amount of samples and stores in the array. Then it averages all those samples together into a larger rolling average. So in essenece, you could get a rolling average for each minute and then average those into the hour rolling average.
IF (FIRST_PASS != TRUE) OR (IN5)THEN PTR := 0; PTR2 := 0; ARRAY_FULL := FALSE; ARRAY_FULL2 := FALSE; NUM_VALUES := IN2; NUM_VALUES2 := IN3; FIRST_PASS := TRUE;ENDIF;VALUE := IN1;(* Reset Logic - Clears the Array *)IF (IN5)THEN X := 0; WHILE (X <= MAX(NUM_VALUES, NUM_VALUES2)) DO '^/ARRAYA'[X][1] := 0; '^/ARRAYA'[X][2] := 0; X := X + 1; END_WHILE; ARRAY_SUM := 0; ARRAY_SUM2 := 0; '^/RESET.CV' := FALSE;ENDIF;(* Enable - Do the Average *)IF (IN4)THEN PTR := PTR + 1; IF (PTR > NUM_VALUES) THEN ARRAY_FULL := TRUE; PTR := 1; ENDIF; LAST_VALUE := '^/ARRAYA'[PTR][1]; '^/ARRAYA'[PTR][1] := VALUE; IF (ARRAY_FULL = TRUE) THEN ARRAY_SUM := ARRAY_SUM + VALUE - LAST_VALUE; AVERAGE := ARRAY_SUM / NUM_VALUES; ELSE ARRAY_SUM := ARRAY_SUM + VALUE; AVERAGE := ARRAY_SUM / PTR; ENDIF; (* Average the averages *) IF (PTR = NUM_VALUES) THEN PTR2 := PTR2 + 1; IF (PTR2 > NUM_VALUES2) THEN ARRAY_FULL2 := TRUE; PTR2 := 1; ENDIF; LAST_VALUE2 := '^/ARRAYA'[PTR2][2]; '^/ARRAYA'[PTR2][2] := AVERAGE; IF (ARRAY_FULL2 = TRUE) THEN ARRAY_SUM2 := ARRAY_SUM2 + AVERAGE - LAST_VALUE2; AVERAGE2 := ARRAY_SUM2 / NUM_VALUES2; ELSE ARRAY_SUM2 := ARRAY_SUM2 + AVERAGE; AVERAGE2 := ARRAY_SUM2 / PTR2; ENDIF; ENDIF; (* Calculate Weighted Average *) IF (ARRAY_FULL2 = TRUE) THEN WEIGHTED_AVERAGE := ((AVERAGE2 * (NUM_VALUES2 - 1)) + AVERAGE) / NUM_VALUES2; DIFF := VALUE - LAST_VALUE2; ELSE IF (ARRAY_FULL = TRUE) THEN WEIGHTED_AVERAGE := ((AVERAGE2 * (PTR2 - 1)) + AVERAGE) / PTR2; DIFF := 0; ELSE WEIGHTED_AVERAGE := AVERAGE; DIFF := 0; ENDIF; ENDIF;ELSE AVERAGE := 0; AVERAGE2 := 0; WEIGHTED_AVERAGE := 0;ENDIF;OUT1 := WEIGHTED_AVERAGE;'OUT1.ST' := 'IN1.ST';OUT2 := AVERAGE;'OUT2.ST' := 'IN1.ST';OUT3 := AVERAGE2;'OUT3.ST' := 'IN1.ST';OUT4 := PTR;OUT5 := VALUE;OUT6 := ARRAY_SUM;OUT7 := PTR2;OUT8 := ARRAY_SUM2;OUT9 := NUM_VALUES * NUM_VALUES2;OUT10 := LAST_VALUE2;OUT11 := DIFF;
Andre Dicaire
In reply to Andre Dicaire:
I'm wondering is a DT block could be used. Based on KeithHarms approach, you would initialize total sum from the current value. The Deadtime block would also be initialized using the Follow parameter. Average value is (Total + Current - DT/OUT)/number of values.
The Deadtime block is set to 1 hour giving you the value 1 hour ago. The logic would look like this:
The ACT, ADD, SUB and DIV blocks could all move into a CALC block. Here the ACT block simply calculates the starting SUM value which is SGGN/OUT.CV * DT/DEAD_TIME which is in seconds, or 3600.
I placed a filter on the input of the DT block, though this may not be necessary or wanted. My thought is the filter would attenuate older values while new values would have a slightly larger influence.
I've set this module at 1 second scan rate. The EXEC_TIME is at 410usec on a PK controller. The DT block should be very efficient in terms of managing its internal array.
Just a thought.
In reply to KeithHarms:
Hi Andre, I believe the DT block is 30 samples. There is a really old KBA (AUS1-198-990902105308) that talks about longer applications might need multiple DT's chained together to get more samples.
This works, not sure if it meets your intent though, long thread couldn't make it through
In reply to Matt Forbis:
Seeing the amount of replies, it seems to me that Emerson should consider to develop a rolling average function block based on the great ideas presented in this discussion.
In reply to Pieter Degier:
This is similar to what I've used in the past:
I've used function blocks to provide better understanding and facilitate replication for those who want to test it.
The SIGNAL GENERATOR is configured to provide a SINE wave for testing purposes. Next is trend of this logic running into a real PK controller :
BLACK pen is the average value, sitting in the middle of the other four samples, as it is expected.
OFFSET (sample time spacing) can be changed online but then, average calculation is reset because using values from the past will not be valid.
In reply to gamella: