• Not Answered

How do I create a rolling average for the last hour?

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

15 Replies

  • Several years ago I create an algorithm to solve this in a PLC which APPROXIMATES a rolling average, and have been using it in all sorts of controls applications since. The benefit to this is that it only requires a few registers and does a good job as long as the sample count is high and the signal is changing slowly from one sample to the next. This method eliminates the need to store all of the samples and ASSUMES that the new sample is not too much different from the average. As always test this heavily to make sure it fits your application.

    At a some sample rate which will produce several thousand samples per averaging period execute the following:

    SampleSum = SampleSum + Sample
    If SampleCount < MaxSamples then SampleCount = SampleCount + 1
    Average = SampleSum / SampleCount
    SampleSum = SampleSum - Average

    In your application I would place this in a module executing at 1 sec (Sample rate) using an CALC block with outputs for SampleSum and Average and parameters at the module level for MaxSamples and SampleCount (allows adjustment and reset). With the sample rate of 1 sec the Max Samples parameter would be set to 3600 to achieve a 1 hr average.
  • 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;

  • EXEC_TIME is the CPU time the module takes to execute. It is informational and is not used by Scan Time multiplier. The Scan Time multiplier uses the Module's configured scan time as defined in the module. You can read that value with MODNAME/PERIOD.CV. The specified block will execute once every x number of scans as specified. This is intended to reduce loading of a module and should not be used to delay logic that requires fixed time intervals.

    EXEC_TIME is calculated as the difference between the start time and end time of the module when it executes. It does not matter what the scan time of the module is, it will take X amount of time for the CPU to process the module. The Overall CPU load of a module is a function of its EXEC_TIME and Scan Time. The Scan Multiplier can help reduce this CPU load by reducing the amount of logic processed in each scan.

    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. 

    Andre Dicaire

  • In reply to Andre Dicaire:

    Andre is correct in that a function block execution would be possible in this form with the exception that the output of the SUB block needs to be connected back to the upper input of the ADD.
    In its current form the logic would not generate a true average as the sum is 3600 * the input at time(0) plus the input at time(now) minus the input at time(now - 3600) and would never truly represent the signal over the last hour, as it is biased by the input at initialization.
    An action block that copies the output of the SUB block to the parameter "SUM" set as the last block to be executed in the module would correct the issue. With this change the initialization logic becomes un-necessary as well.

    When working with large time delays I was not sure of the impact to controller memory when using the DT block particularly with older generation(s) of hardware, which is why I have preferred to use a CALC block type of implementation in the past. I will have to take a look and see if this is a better implementation.
  • In reply to Andre Dicaire:

    Andre, I confirm DT approach can be used an it works very well. I used it several times with no issues. DT approach provides easy way to manage initial state (after download) or any required reset; using FOLLOW parameter.

    The main drawback is the required number of samples. If it is small (<16) one DT for each sample can be used but if it is larger, use of arrays and code will be a better option.
  • In reply to KeithHarms:

    Thanks KeithHarms. This was more of a thought and I threw that together as an example. You're right. What I missed is that the SUM value needs to adjust over time until it is comprised of valid data. After 1 hour, the SUM parameter should contain the sum of the last 3600 values.

    Another initialization could be to do a cumulative average based on the number of new samples until the FIFO is full, i.e. the DT block is outputting values from 1 hour ago. A counter would be used for the average and SUM would aggregate a new value every scan without subtracting the value from an hour ago. Once the counter completes, the DT OUT is brought in to keep the SUM the result of the last x number of values.

    I would also add a Status criteria that sets the Status of the resulting average as it initializes until a statistically valid number of samples are included. Also, if the input goes bad, the Average should have its status shift to Uncertain if the number of BAD samples becomes significant. Or not. Depends on what this value is used for.

    Gamella points out that if you have more than 16 samples to store, the DT should not be used. That brings up a good question. How many data values does the DT block have to store data. The Deadtime block can be set from 0 to 32400 seconds or up to 9 hours of deadtime. However, I don't think the block has 32400 registers. Some where in the back of my mind is a value of 120 registers, but I can't seem find that information anywhere. That can be found out with a simple module I guess.

    But assuming it is 120 registers, then to do an hour's worth of deadtime, the block would need to store a value every 30 seconds. To avoid aliasing, the input to the DT should have a filter set appropriately (in this case, about 20 Seconds.) If the process time constant is slow (> 30 seconds), this would not be an issue. For a faster process, the filter would attenuate transients, and avoid a false "alias" signal caused by a 30 second harmonic in the signal. for a faster process, the FIFO needs more registers. Adding several DT blocks in series would provide a larger FIFO buffer. Using 4 DT blocks, each set to 15 minutes, would give you better resolution.

    The benefit of the DT block is that is is a compiled code and by that fact it is significantly more efficient than manipulating an array with expression logic, and significantly easier to deploy. It will also adapt based on the module execution rate to always give you the configured Dead_Time.

    The SMA (Simple Moving Average) is a filter that smooths a curve. Have you considered if a simple filter would provide adequate results? What is the application that benefits from a Simple Moving Average versus a First Order Filter?

    Andre Dicaire

  • In reply to Andre Dicaire:

    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.

  • In reply to Andre Dicaire:

    This works, not sure if it meets your intent though, long thread couldn't make it through

    Logic OverviewCalc 1Calc 2

  • In reply to Matt Forbis:

    Matt, just confirmed this with a test. 30 or maybe 32 looks right. Also confirmed that the DT block does not interpolate the output value. It holds the value stored until the next value pops out of the FIFO.

    Getting back to KeithHarmes solution, he uses the Average value to rather than the FIFO output. Which is a pretty elegant solution, avoiding the DT or FIFO array altogether. One of the knocks against an SMA is that the oldest value can off set the newest value such that the average moves down as the input moves up. By using the average as the oldest value, the average is more responsive to current behavior of the input.

    Benji gives us a solution using an Integrator block to create a number of averages based on the sample period and then averages these for an overall average across the defined time period. This gives you both an average for say an hour, but also an average for the last minute, assuming you have 60 samples per hour. The downside, it if is one, is that this drives a new value based on the sample period. But then again, this is an average over time. An SMA reflects the average in the middle of the sample period, so for an your, that is 30 minutes ago.

    So which approach is better.
    - A full FIFO stack would become unwieldly as the number of registers increases.
    -compromise is to use two stacks, one per sample period and second to aggregate samples, which is what Benji's solution does except with an Integrator block for the sample Period. This means a new value is updated at the end of each sample period.
    - Simplified SMA using Average as oldest value is quite efficient in design. It is both low on memory usage and low on CPU processing.
    - Simplified SMA using DT block as oldest sits somewhere between Full Fifo and Average for the oldest value.

    it would be interesting to see how each of these would perform on the same input signal. I'm leaning toward KeithHarmes solution and I'm curious if the DT block would provide any benefit. But I also like the idea of the Integrator block providing periodic Averages.

    Good discussion.

    Andre Dicaire

  • In reply to Andre Dicaire:

    Over the years tried multiple ways in multiple systems, and my guess would be that the (sum of all the ways / number of ways) will result in the average of the ways.
  • 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:

    I agree Pieter! It's always been a little bit of a mystery to me that Emerson has a number of products across it's device and flow computer portfolio that offer these kinds of calculations out of the box, yet DeltaV doesn't provide anything. Granted, DeltaV provides all the tools to configure what you might want, and there's plenty of possible implementations of the maths here, but I think it reasonable that some solution should be there out-of-the-box. I'm not sure that finding yourself 100 lines deep in structured text to provide a relatively common requirement is really ideal.
    Some great ideas here, congrats to the contributors!
  • In reply to Andre Dicaire:

    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:

    The DT block does hold 30 samples, including the sample 29 scans ago up to the current sample. The highest deadtime you can assign, such that every value is stored, is 29 scans. At least, that is what my test just showed me.