Algoritmo eficiente / estructura de datos para calcular promedios móviles


9

Actualmente estoy desarrollando un sistema gráfico LCD para mostrar temperaturas, flujos, voltajes, potencia y energía en un sistema de bomba de calor. El uso de una pantalla LCD gráfica significa que la mitad de mi SRAM y ~ 75% de mi flash han sido utilizados por un búfer de pantalla y cadenas.

Actualmente estoy mostrando cifras mínimas / máximas / promedio de energía A la medianoche, cuando se restablece la cifra diaria, el sistema verifica si el consumo del día está por encima o por debajo del mínimo o máximo anterior, y almacena el valor. El promedio se calcula dividiendo el consumo de energía acumulado por el número de días.

Me gustaría mostrar el promedio diario durante la última semana y mes (4 semanas para simplificar), es decir, un promedio móvil. En la actualidad, esto implica mantener una matriz de valores durante los últimos 28 días y calcular un promedio sobre la matriz completa por mes y los últimos 7 días por semana.

Inicialmente estaba haciendo esto usando una matriz de flotadores (ya que la energía está en la forma "12.12kWh"), pero estaba usando 28 * 4 bytes = 112 bytes (5.4% de SRAM). No me importa tener solo un punto decimal de resolución, así que cambié a usar uint16_t y multiplicar la cifra por 100. Esto significa que 12.12 se representa como 1212 y divido por 100 para mostrar.

El tamaño de la matriz ahora se ha reducido a 56 bytes (¡mucho mejor!).

No hay una forma trivial de reducir la cifra a un uint8_t que pueda ver. Podría tolerar la pérdida de un lugar decimal ("12.1kWh" en lugar de "12.12kWh"), pero el consumo es con frecuencia superior a 25.5kWh (255 es el valor más alto representado por un entero sin signo de 8 bits). El consumo nunca ha sido inferior a 10.0kWh o superior a 35.0kWh, por lo que posiblemente podría restar 10 de las cifras almacenadas, pero sé que algún día superaremos estos límites.

Luego probé el código para empaquetar valores de 9 bits en una matriz. Esto proporciona un rango de 0-51.2kWh y utiliza 32 bytes en total. Sin embargo, acceder a una matriz como esta es bastante lento, especialmente cuando tiene que iterar sobre todos los valores para calcular un promedio.

Entonces mi pregunta es: ¿hay una forma más eficiente de calcular un promedio móvil con tres ventanas: de por vida, 28 días y 7 días? Eficiencia significa menor en términos de uso de SRAM, pero sin la penalización de un gran código. ¿Puedo evitar almacenar todos los valores?


¿Desea calcular un promedio móvil sobre ventanas específicas o una estimación / aproximación del promedio?
asheeshr

Quiero un promedio móvil en una ventana de 7 días y 28 días.
Cybergibbons

usted podría utilizar una resolución de 0.2kWh (dividir y multiplicar con el factor 5) y aún así obtener 0-51.2kWh gama de 8 bits
trinquete monstruo

Puede terminar colocando cadenas y otras constantes en RAM externa o Flash externo. Consulte "¿Qué puedo hacer si me quedo sin memoria Flash o SRAM?" .
David Cary

Respuestas:


2

Si sus datos tienen una desviación estándar baja, entonces un método sería sumar valores sobre la ventana y luego restar la media de la suma, mientras agrega el nuevo valor.

Esto funcionaría bien si no hay valores atípicos , lo que llevaría al error agregado que tiende a cero con el tiempo.

//Pseudocode

count=0
while new_reading and count<7:
    sum += new_reading        //Calculate the sum of first 7 values
    count++

while new_reading:            //Loop till new readings available
    avg = sum / 7             //Calculate average
    sum -= avg                //Subtract average from sum
    sum += new_reading        //Add next reading to sum
    print avg

2

puedes usar un método diferente, mantienes el promedio actual y luego haces

average = (weight1*average+weight2*new_value)/(weight1+weight2);

No es un promedio móvil real y tiene una semántica diferente, pero puede satisfacer sus necesidades de todos modos.

para un método de cálculo más eficiente para su solución de 9 bits por valor, podría mantener los 8 bits más altos de los valores en una matriz y separar los bits menos significativos:

uint8_t[28] highbits;
uint32_t lowbits;

para establecer un valor necesitas dividirlo

void getvalue(uint8_t index, uint16_t value){
    highbits[index] = value>>1;
    uint32_t flag = (value & 1)<<index;
    highbits|=flag;
    highbits&=~flag;
}

resultando en 2 desplazamientos un AND y un OR y un no

Para calcular el promedio, puede usar varios trucos de bits para acelerarlo:

uint16_t getAverage(){
    uint16_t sum=0;
    for(uint8_t i=0;i<28;i++){
        sum+=highbits[i];
    }
    sum<<=1;//multiply by 2 after the loop
    sum+=bitcount(lowbits);
    return sum/28;
}

puede usar una cuenta de bits paralela eficiente parabitcount()


1
¿Puede explicar más cómo esto me permitiría calcular el promedio de 7 y 28 días?
Cybergibbons

He utilizado este enfoque para suavizar valores analógicos ruidosos antes, y ciertamente fue bastante efectivo. Sin embargo, no necesitaba mucha precisión, ya que los valores resultantes se estaban poniendo a través de un cuantificador muy grueso. Tampoco necesitaba promedios históricos.
Peter Bloomfield

Esto no permite calcular el promedio de una ventana específica.
asheeshr

@Cybergibbons se pueden utilizar diferentes pesos para aproximarse a la ventana para que los viejos valores se vuelven insignificantes antes o después, o mantener a los 7 días de la ventana de 7 días y el promedio móvil de 28 días de media
monstruo de trinquete

1

¿Qué tal si solo almacenamos la diferencia del valor anterior? En electrónica hay un concepto similar llamado convertidor Delta Sigma, que se utiliza para convertidores DA / AD. Se basa en el hecho de que la medición anterior está razonablemente cerca de la actual.


Otra idea interesante. Desafortunadamente, no estoy seguro de que el consumo de energía siempre sea así, ya que es un sistema de bomba de calor y un día podría tomar 30kWh, los siguientes 10kWh. Realmente necesito recopilar datos y ver.
Cybergibbons

0

¿Por qué no podría simplemente agregar los valores juntos tan pronto como los obtenga? Entonces, lo que quiero decir es que obtienes el valor para el día 1, lo divides por 1 y lo almacenas y el 1 en algún lugar. Luego, multiplica el 1 por el valor y lo sumas al siguiente valor y los divides por 2.

Hacer este método crearía un promedio móvil con dos o tres variables como puedo pensar. Escribiría un código, pero soy nuevo en stackexchange, así que tengan paciencia conmigo.


No entiendo cómo se trata esto con la ventana de 7 días y 28 días.
Cybergibbons

Haga un seguimiento de los valores anteriores y siguientes y siga sumando y restando de su promedio de ejecución ...
Aditya Somani

1
Entonces, ¿estoy de vuelta en el estado de necesidad de recordar 27 días de historia, sin duda?
Cybergibbons

He estado pensando y tienes razón. Así que técnicamente hace que mi respuesta sea incorrecta. Estoy invirtiendo más tiempo y paciencia en ello. Quizás algo fuera de la caja. Te avisaré si se me ocurre algo. Hacemos algo así mucho en mi lugar de trabajo. Déjame preguntar por ahí. Perdón por la confusión.
Aditya Somani

0

¿Existe una forma más eficiente de calcular un promedio móvil con ... 28 días y 7 días? ... necesita recordar 27 días de historia ...?

Puede acercarse lo suficiente para almacenar 11 valores en lugar de 28 valores, tal vez algo como:

// untested code
// static variables
uint16_t daily_energy[7]; // perhaps in units of 0.01 kWh ?
uint16_t weekly_energy[4]; // perhaps in units of 0.1 kWh ?

void print_week_status(){
    Serial.print( F("last week's total energy :") );
    Serial.println( weekly_energy[0] );
    int sum = 0;
    for( int i=0; i<4; i++ ){
        sum += weekly_energy[i];
    };
    Serial.print( F("Total energy over last 4 complete weeks :") );
    Serial.println( sum );
    int average_weekly_energy = sum/4;
    int average_daily_energy = average_weekly_energy/7;
    Serial.print( F("Average daily energy over last 4 weeks :") );
    Serial.println( average_daily_energy );
}
void print_day_status(){
    Serial.print( F("Yesterday's energy :") );
    Serial.println( daily_energy[0] );
    Serial.print( F("average daily energy over the last 7 complete days: ") );
    int sum = 0;
    for( int i=0; i<7; i++ ){
        sum += daily_energy[i];
    };
    int average = sum/7;
    Serial.println( average );
}

En otras palabras, en lugar de almacenar cada detalle de cada día durante los últimos 27 días, (a) almacene 7 o más valores de información diaria detallada de los últimos 7 días, y también (b) almacene 4 más o menos "resumidos" valores de información total o promedio para cada una de las últimas 4 semanas.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.