Teoría de la Información y Teoría de Códigos
Códigos Convolucionales
Carlos Sánchez Sánchez
3º de Grado en Ingeniería Informática
Índice
1. Introducción
5
2. Aspectos Generales Generales
6
3. Codificación
8
4. Decodificació Decodificación: n: Algoritmo de Viterbi. Viterbi.
14
5. Distancia Libre
19
6. Capacidad Capacidad Correctora .
21
7. Mejoras de los Códigos Códigos Convoluciona Convolucionales les
26
8. Aplicaciones
28
Anexo A. Programa de Simulación Codice.
29
Bibliografía
32 32
3
1.
Introducción
En este trabajo se hace una introducción a los códigos convolucionales como un tipo de códigos correctores de errores diferente a los códigos de bloques. La principal diferencia entre ellos es la introducción del concepto de memoria, esto es, la codificación en un momento dado no dependerá solo de la palabra a codificar, también de las codificadas anteriormente. Estos códigos presentan una gran ventaja frente a los de bloques en canales con mucho ruido (alta probabilidad de error). Las comunicaciones inalámbricas o comunicaciones con satélites destacan entre sus usos. Los códigos convolucionales son un campo de trabajo abierto dentro de la teoría de códigos y actualmente se pueden encontrar desde distintos enfoques y definiciones. Aquí se tratará de dar una visión general tratando de destacar los aspectos que suponen una diferencia respecto a los códigos de bloques. En todos los casos posibles se han incluido ejemplos de su funcionamiento. Para los conceptos que por su complejidad excedían el objetivo del trabajo se han añadido algunas referencias. En primer lugar se describirán las características y parámetros que definen estos códigos. En la sección 3 se explica la codificación, tanto desde un punto de vista matemático como desde el necesario para su implementación en hardware. Los tres apartados siguientes se dedican a la decodificación. En el apartado 4 se muestra el proceso de decodificación, para el que se describe el agoritmo de Viterbi. En el 5 se introduce el concepto de distancia, que determinará la capacidad correctora del código y en el 6 se mostrarán algunas cotas y ejemplos de esta capacidad. También se realizará una comparación teórica y práctica de estos códigos con códigos de bloques desde el punto de vista de los errores que pueden corregir. En la sección 7 se mostrará un repaso breve de algunas técnicas que permiten mejorar la codificación mediante códigos convolucionales. También, de forma breve, algunos usos de códigos convolucionales en combinación con otros códigos que abren nuevas vías en la teoría de códigos. Finalmente, en la última sección se dará un repaso por algunas aplicaciones y usos prácticos de los códigos convolucionales haciendo especial hincapié en la importancia que han tenido para la historia de la exploración espacial Junto al trabajo se ha realizado un pequeño simulador de códigos que permite realizar la codificación, transmisión y posterior corrección y decodificación. Los resultados de este programa se utilizarán en algunas de las secciones del trabajo y una descripción de su funcionamiento puede ser consultada en el Anexo A.
5
2.
Aspectos Generales
En los códigos de bloques el mensaje se dividía en palabras y la codificación era la transformación de cada palabra de información u ∈ Fq k en una palabra del código x ∈ Fq n mediante el producto por la matriz generadora G de forma que x = u · G. En este caso la codificación es constante en el tiempo. Sin embargo, en los códigos convolucionales, la codificación de una palabra variará en función de las palabras transmitidas anteriormente. El número de palabras anteriores de las que depende la codificación se denomina memoria m del código. La introducción a los códigos convolucionales fue obra de P. Elias 1955 como alternativa a los códigos de bloques existentes. Posteriormente, en 1970 Shun Lin trato de forma analítica la codificación convolucional, lo que impulsó el uso de estos códigos, un desarrollo desde el punto de vista algebraico fue realizado por G.D. Forney Jr. en el mismo año y posteriormente fue reformulado por R. McEliece en 1998. [6] Otros nombres dados a estos códigos son los de códigos recurrentes o códigos de árbol, debido a que es posible realizar una representación gráfica estructurada en forma de árbol. En estos códigos, entonces, a partir de la palabra que se codifica y las m anteriores se generarán símbolos de redundancia. Sin embargo, a diferencia de los códigos de bloques, no se envían estos símbolos como un añadido a la palabra a codificar. Es habitual que sean estos los únicos símbolos enviados. Teniendo esto en cuenta, en el caso de un código convolucional C para un alfabeto A se tienen tres parámetros (n,k,L) , dos equivalentes a los vistos para los códigos de bloques y la restricción de distancia: k: número de símbolos de la palabra de datos que se codifica en cada paso. n: número de símbolos de la palabra codificada a partir de k bits. L: restricción de distancia. Siendo m la memoría del código, L = m + 1 es el número de fases en las que una palabra ejerce influencia sobre la salida. Es decir, influye sobre sí mismo y m más. Es también habitual que la restricción de distancia se represente con la letra K , pero aquí se usará la notación L para distinguirla del número de bits de la palabra a codificar. En ocasiones en lugar de darse en cantidad de símbolos se hace en bits. Partiendo de estos parámetros es posible entender los códigos de bloques como códigos convolucionales con m = 0, pues este es el caso en el que no se tiene en cuenta ninguna de las codificaciones anteriores. El ratio o tasa del código, definida de igual forma que para los códigos de bloques es r =
BITS quesecodifican k = BIT S enviados n
Sin embargo, generalmente los símbolos se codifican uno a uno, por tanto k = 1 y r = n1 , sin agrupación en bloques. Esto permite transmitir grandes corrientes de bits cuyas salidas se combinarán de forma preestablecida en la codificación. Será en estos códigos, por ser los más sencillos y más utilizados, en los que nos centraremos principalmente en este trabajo. 6
En este caso tenemos que para una palabra de un símbolo u i enviada en el momento i-ésimo se obtiene una serie de bits x 1 , x2 , ..., xn . Y, teniendo en cuenta la memoria, la codificación de esta palabra dependerá de los símbolos u i , ui 1 , ..., ui m . −
−
La palabra codificada será siempre una combinación lineal de u i , ui 1 , ..., ui m , y la entrada nula (0, ..., 0) siempre da la salida nula. Por tanto, pese a no ser códigos de bloques, sí que se pueden estudiar como códigos lineales. −
−
Los códigos convolucionales pueden tomar sus elementos de cualquier cuerpo finito, sin embargo, el más común es F2 y será el utilizado en los ejemplos.
7
3.
Codificación
La realización física de un codificador puede realizarse mediante circuitos secuenciales lineales. Se pueden utilizar L registros cuyas salidas se suman de forma que se obtiene una palabra del código para cada entrada. Con la entrada de una nueva palabra a codificar las anteriores se desplazan al siguiente registro y se obtiene una nueva palabra codificada. Tomamos como ejemplo el código k = 1 en el que se desea que al codificar el bit i-ésimo se obtengan dos bits. El primero será la suma del bit a codificar y los dos anteriores y el otro la suma del actual y el que se codificó en la penúltima codificación. El codificador sería el de la siguiente imagen:
El número de registros utilizados es 3 y este es el número de entradas que afectan a cada salida, por tanto la restricción de distancia L es 3 y la memoria m es 2. Se hace referencia a la palabra actual y, como mucho, a las dos anteriores. Una visión de caja negra de este codificador implicaría una entrada de la palabra ui del código y la salida de dos bits que forman la palabra codificada. Sin embargo, es posible expresar cada una de las salidas como una función, denominada función generadora, de ui ,...,ui m . En el caso anterior se tendrían: −
x1 = 1 · ui + 0 · ui 1 + 1 · ui
−
x2 = 1 · ui + 1 · ui 1 + 1 · ui
−
−
−
2 2
De este modo, es posible establecer el codificador G del código con k = 1 como una aplicación lineal entre los subespacios FL y Fn de la siguiente forma: G :
L
F2
−→ Fn2
(ui , ui 1, ..., ui −
m
−
) −→ (x1 , x2 , ..., xn )
donde i indica el momento de la codificación en el que estamos. Para el caso visto, en concreto: 8
G :
3
2
F2 → F2
(ui ui 1 , ui 2) −→ (x1 , x2 ) −
−
odigo convolucional son, por tanto, el subespacio generado por Las palabras del [n, 1, L] − c´ la imagen de esta aplicación C ≡ ImgG Y el resultado de la codificación será el vector formado por los elementos m
u x = i
(m−k)
· (gij )
k=0
siendo gi las ecuaciones para cada una de las salidas. Para verlo más claro tomamos como ejemplo el [2, 1, 3]-código convolucional visto antes con: g1 = (1, 0, 1) g2 = (1, 1, 1) Si codificamos el vector (1, 0, 1, 0), la salida será: Al entrar el primer elemento (1), se tienen en cuenta las dos anteriores entradas ficticias como ceros, por tanto, en t = 1: x11
x12
1 = (1, 0, 0) 0 = 1 + 0 + 0 = 1 1 1 = (1, 0, 0) 1 = 1 + 0 + 0 = 1 1
La salida sería: x1 = (1, 1)
Para la segunda entrada (0), se tendrá que las entradas anteriores son 1 y 0: x21
x22
1 = (0, 1, 0) 0 = 0 + 0 + 0 = 0 1 1 = (0, 1, 0) 1 = 0 + 1 + 0 = 1 1
Y como consecuencia: x 2 = (0, 1)
Siguiendo del mismo modo se tendrá como salida (x1 , x2 , x3 , x4 ), el vector (1, 1, 0, 1, 0, 0, 0, 1). Es posible ver en el ejemplo que para la codificación de los primeros dos bits (1, 0) se tiene como resultado (1, 1, 0, 1). Sin embargo, para los dos siguientes, aún siendo iguales, la salida 9
es (0, 0, 0, 1). Esto se debe, como se ha dicho, a que la salida depende también de las entradas anteriores. La codificación de un mensaje con N palabras ui de k bits se puede generalizar como la aplicación tomando polinomios: G(z ) : F2 [z]k → F2 [z]n u(z ) =
N −1 i=0
ui z i −→ (x1 (z ), ..., xn (z )) | x j = u(z ) · g j (z )
Donde u(z ) es un vector de k polinomios de grado N − 1 que representan la entrada. Y los (x1 (z ), x2 (z ), ..., xn (z )) son n polinomios de salida resultado del producto de la entrada por una matriz de k × n polinomios de grado hasta m cuyas columnas son los g j . Ejemplo de codificación con k = 1
Para ver esto mejor se realiza utilizando este método la misma codificación realizada antes para el código con k = 1: G(z ) : F2 [z] → u(z ) =
N −1 i=0
F2 [z]
n
ui z i −→ (x1 (z ), x2 (z ))|x j = u(z ) · g j (z )
Los polinomios generadores de esta aplicación son los polinomios de grado m con los coeficientes de las secuencias generadoras: g1 (z ) = 1 + z 2 g2 (z ) = 1 + z + z 2 . Sin embargo suelen representarse con notación octal. Para este caso 101 2 = 58 y 111 2 = 78 , sería por tanto un código con restricción de distancia 3, longitud 2 y polinomios (5, 7). La expresión polinómica de la entrada (1, 0, 1, 0) codificada anteriormente es u(z ) = (1 + z 2 ). Por lo que la codificación se realiza del siguiente modo: x1 (z ) = (1 + z 2 ) · (1 + z 2 ) = 1 + z 4 −→ (1, 0, 0, 0, 1) x2 (z ) = (1 + z 2 ) · (1 + z + z 2) = 1 + z + z 3 + z 4 −→ (1, 1, 0, 1, 1) E intercalando ambas salidas tenemos (1, 1, 0, 1, 0, 0, 0, 1, 1, 1). Esta salida corresponde con la anterior, excepto porque se han añadido algunos bits al final. Esto se debe a que esta codificación corresponde a la anterior añadiendo ceros al final de modo que todos los registros estén a cero al acabar. Esto se denomina codificación zero-tailed y el número de ceros que se deben añadir es igual a m. Por tanto la salida anterior se corresponde a codificar (101000), que sería (1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0), teniendo en cuenta los ceros al final para los grados restantes. [13]
10
Esta codificación supone una reducción del tasa del código, que ahora será, para un mensaje de m bits igual a n (N N +m) . ·
Para mensajes cortos esto supondría una gran pérdida pero para secuencias muy largas de bits el aumento es despreciable. Otra opción, aunque más compleja, se denomina código convolucional con bits de cola (tailbiting) [13]. En estos se establece el inicio del codificador en el mismo estado que se encontraría al final, lo cual complica la decodificación pero elimina la disminución de la tasa del código. Ejemplo de codificación con k > 1
Esta forma de codificación es válida también para códigos con k > 1. Por lo que si estuvieramos hablando de un código con k = 2, n = 3 y L = 2 para un mensaje de 6 bits se tiene que N = 3. G(z ) : F2 [z]2 → F2 [z]3 u(z ) =
2 i i=0 ui z
−→ (x1 (z ), ..., xn (z )) | x j = u(z ) · g j (z )
La matriz para este codigo será: G(z ) =
1 + z z
z 1
1 + z 1
Lo que significa que la primera salida será la suma del primer y segundo elemento de ui con el segundo de ui 1 . La segunda salida la suma del segundo elemento de ui con el primero de ui 1 . Y la tercera salida de nuevo el primer y segundo elemento de ui pero ahora con el primero de u i 1 . −
−
−
Si se desea transmitir el mensaje u = 110110, este queda dividido en las palabras de 2 bits u0 = 11, u1 = 01 y u2 = 10, y se tiene: u(z ) = Por tanto:
1 i i=0 ui z
= (1, 1) · z 0 + (0, 1) · z 1 + (1, 0) · z 2 = (1 + z 2 , 1 + z )
x(z ) = u(z ) · G(z ) = (1 + z 2 , 1 + z )
1 + z z
z 1
1 + z 1
= (1 + z 3, 1 + z 3 , z 2 + z 3)
Y las xi resultantes, tomadas como los coeficientes de los polinomios son: x0 = (1, 0, 0, 1) x1 = (1, 0, 0, 1) x2 = (0, 0, 1, 1) Por lo que el mensaje codificado, al reordenar las salidas es: x = (1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1)
11
Diagrama de Trellis
Para facilitar la comprensión de la codificación, se puede utilizar una representación gráfica que permite ver las salidas asociadas con el paso del tiempo. Se denomina diagrama de Trellis. En la siguiente imagen se ve el ejemplo para el anterior codificador.
En el diagrama de Trellis se ven las entradas anteriores a la actual como estados que se representan en el eje vertical. Por tanto el número de estados sería de q m . En este caso, 22 = 4, ya que es un código binario de memoria 2. En el eje horizontal se muestran las distintas entradas. A cada una corresponde una flecha, con linea continua si la entrada i es un 0 y discontinua para las entradas 1. Junto a cada flecha se indica la salida correspondiente a esa entrada. En un primer momento el estado es 00, ya que se suponen dos entradas anteriores de 0. Si la primera entrada ui es 0, el estado al que se pasa es, de nuevo, 00 (tanto esta entrada como la 1 1 (0, 0, 0) 0 , (0, 0, 0) 1 = (0, 0). anterior son cero) y la salida es (x1 , x2 ) = 1 1
En caso de que la primera entrada sea u = 1. El nuevo estado será (1, 10), 1 de esta entrada 1y 0 de la entrada anterior. La salida en este caso es (x , x ) = (1, 0, 0) 0 , (1, 0, 0) 1 = i
1
2
1
1
(1, 1). El diagrama de Trellis para este paso es el de la imagen.
Aparece marcada en rojo la entrada uno estudiada en ejemplos anteriores, y, siguiendo el mismo mecanismo, se llega al diagrama de Trellis para la entrada (1010) con ceros de cola. 12
Y la salida coincide, de nuevo, con la vista anteriormente: (1, 1, 0, 1, 0, 0, 0, 1, 1, 1). Esta idea de que un estado se corresponde con las entradas anteriores es posible representarla también con un diagrama de estados.
13
4.
Decodificación: Algoritmo de Viterbi.
La decodificación consiste en encontrar la secuencia de palabras del código correspondiente a la secuencia recibida. Dado que pueden haberse producido errores, se buscará la palabra más probable. Esta será aquella para la que la distancia de Hamming entre su salida y la secuencia recibida sea menor. La decodificación es más compleja que la codificación. Es posible hacerla calculando todas las distancias entre la secuencia recibida y las posibles secuencias transmitidas, eligiendo después la que tiene una menor distancia de Hamming. Sin embargo este método, cuando se tienen grandes cantidades de información, hace que el número de cálculos sea demasiado alto como para ser práctico, pues implica una carga computacional excesiva. Como solución a esto una de las opciones que se utilizan es el algoritmo de Viterbi . Este algoritmo, además de ser sencillo, es capaz de encontrar la entrada con mayor probabilidad de acierto. Un decodificador basado en este método almacena cada secuencia de bits recibida y calcula la secuencia de estados más probable siguiendo los caminos en el diagrama de Trellis con menor distancia acumulada, descartando los caminos no viables y los menos probables. Una vez encontrado este camino se obtiene la cadena de bits asociada a esas transiciones. Tomemos como ejemplo la recepción de los bits 1010 codificados con el codificador de polinomios (5, 7) utilizado hasta ahora. El diagrama de Trellis de la codificación es el visto antes y el resultado la secuencia 11010001. Supongamos que en la transmisión se ha producido un error y se recibe 11000001. El decodificador partirá del estado cero, y calculará todos los posibles caminos desde ahí, a cada uno de los estados resultantes les asignará un valor llamado path metric. Vamos a tomar que este valor sea la distancia de Hamming entre la salida asignada a esa transición y el par de bits recibidos (branch metric). Para los dos primeros bits recibidos se tiene que d(00, 11) = 2 y d (11, 11) = 1, por tanto:
En el nuevo paso se explorarán las rutas que parten de todos los los estados a los que se ha llegado, en este caso 00 y 01. Al valor de cada estado se le sumará la distancia entre los 2 bits siguientes y las nuevas transiciones : 14
En una nueva iteración, habrá varios caminos para llegar a un nuevo estado. Se selecciona el que llegue con un menor valor asociado. En caso de que sean iguales, se selecciona uno cualquiera.
Aparecen en verde los valores de los caminos seleccionados y en gris los que se han descartado. Y para finalizar, con los dos últimos bits se completa el diagrama de Trellis.
Llegado a este punto, se seleccionara el estado con el valor menor, en este caso 01. Y el mensaje codificado corregido será el único camino superviviente que llega a ese estado, en este caso: 11010010, que coincide con el enviado. 15
La palabra decodificada será el camino que se ha seguido para esa salida 1010, marcado en rojo en el diagrama En este caso no se han añadido los ceros finales. Si fuera el diagrama debería acabar en el estado 00 y sería siempre ese el estado del que se parte, independientemente de su valor. Un posible pseudocódigo para este algoritmo es el siguiente. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
V e ct o r V i te r bi ( V e c t or c o di f ic a da ) { in t n u me ro _e st ad os = ( 2 ^ ( m + 1) ) - 1 V e c to r r e s u l ta d o ;
M a t ri z e s t a do s ( n u m e ro _ e s ta d o s , m + 1 ) estados.RellenarEstados(); V e c to r a e m A n t er i o r ( n u m e r o _ e s t a d o s ) ; V e c to r a e m ( n e s t a d os ) ; aem.fill(-1); aemAnterior.fill(0); aem(0)=0; i nt p a so s = c o d if i ca d a . r o ws ( ) ; M a t ri z c a m i no s ( n u m _ e s t ad o s , p a s os ) ; caminos.fill(-1); caminos(0,0)=1; i nt a c um u la d o r ; f or ( i nt i = 0; i < p as os ; i + +) { f or ( i n t j = 0; j < n e s ta d os ) { i f ( c a mi n os ( i , j ) ! = - 1) 16
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 }
{ f or ( i nt t ra ns = 0 ; t ra ns < =1 ; t ra ns + + ) { a c um u la d or = a e mA n t er i or ( j ) + s um ( a b s ( p a la b ra . r o w ( i ) - s a li d a ( j , k ) ) ; if ( ae m( d es ti no ( j, k) ) > a cu m || a em ( j )== -1 ) { aem ( d es ti no (j ,k ) ) = acum ; c am in os ( i +1 , d es ti no ( j , k) ) = j ; } } } } a e m An t er i o r = a em ; } / /0 s i se u sa z ero - t a il in g i nt a n te r io r = a em . m i n I nd e x ( ); / / el p ri me r b it d el e st ad o e s la ú lt im a e nt ra da c od if ia da . r e su l ta d o ( p as os - 1) = e s ta d os ( e fi n , 0 ) ; f or ( i nt i = p as os ; i > 1; i - -) { r es ul ta do ( i - 2 ,0 ) = e st ad os ( c am in os ( i , ef in ) , 0 ) ; e fi n = c am in os ( i , ef in ) ; } r e t ur n r e s u l ta d o ;
La función devolverá un vector con los bits codificados, para ello hace uso de una matriz de caminos que se va rellenando con las rutas de peso mínimo. Los vectores aem y aemAnterior se usan para almacenar entre pasos cuales son los valores de cada una de estas rutas. Al completar la matriz de caminos, se iterará del final al principió rellenando el vector de resultado con los caminos elegidos, que corresponden al primer bit del estado, dado que este es el bit que entró en la ocasión anterior. Este código necesita almacenar una gran cantidad de información, la relativa al mensaje completo. Para mensajes muy largos, u ocasiones en las que se envían datos de forma continua esto no se puede realizar en la práctica. Una solución es establecer una profundidad de decodificación. En lugar de decodificar el mensaje completo, este se divide en secuencias que se decodifican de forma independiente. Gracias a algunas investigaciones se sabe que una profundidad de 5 · L es suficiente para no perder capacidad de corrección mientras que se reduce el tiempo de respuesta y el espacio necesario 17
para la decodificación. La complejidad del algoritmo de Viterbi depende linealmente de la longitud del mensaje y exponencialmente de la restricción de distancia, dado que el número de estados es 2 L − 1 y se itera en un bucle por cada estado. Como consecuencia de esto para códigos con una memoria muy grande el rendimiento decrece de forma muy rápida y para valores grandes de L el algoritmo de Viterbi no es una opción. Normalmente se restringe a códigos de, como mucho, L = 9. Existen otros algoritmos de decodificación, entre los que se encuentran los algoritmos de decodificación umbral (threshold decoding), que sólo es posible aplicar a algunas clases de códigos y no llegan a una decodificación óptima. También existe otro tipo de algoritmos denominados de decodificación secuencial con mejores resultados, y, pese a no ser tampoco óptimos, son utilizados en códigos con una restricción de memoria para la que no es posible utilizar el algoritmo de Viterbi. Todos estos algoritmos, incluido el de Viterbi, son de decodificación dura ( hard decoding ). Esto significa que el decodificador solamente recibe datos binarios del canal. Los decodificadores de decisión blanda reciben más de un bit en lugar de sólo uno, con lo que se tiene una estimación de como de fiable es la información recibida. Normalmente son suficientes tres bits de decisión blanda. 000 sería el “cero fuerte”, 111 el “uno fuerte” y en medio se tendrían otros valores. Para los algoritmos de decisión blanda, en lugar de utilizar una distancia de Hamming se utiliza otra llamada distancia Euclidea.
18
5.
Distancia Libre
En los códigos convolucionales se ha visto que se puede definir la distancia de Hamming igual que en los códigos de bloques, siendo el número de símbolos en los que difieren dos secuencias de bits codificadas. La distancia libre dfree de un código convolucional se define como la mínima distancia de Hamming entre dos secuencias de bits codificadas y de esta depende el número de errores que es capaz de corregir el código. Del mismo modo que en los códigos de bloques, esto es equivalente a comparar el resto de salidas con la entrada nula. Teniendo esto en cuenta, la distancia libre coincide con el número de unos de la secuencia no nula que, empezando en el estado cero, vuelve a él con la salida con menor número de unos entre todas las posibles. La importancia de la distancia libre se debe a que de este parámetro dependerá la capacidad correctora del código. En el caso del código con polinomios (5, 7) que hemos visto, este recorrido es el correspondiente a la entrada 100 con salida 110111, y por tanto dfree = 5.
Este concepto se puede definir de un modo más formal de la siguiente forma, sea C un código convolucional y wt(x) el peso de una secuencia (distancia de Hamming a la palabra nula) . dfree (C ) = m´ın {wt(x)| x ∈ C, x = 0} odigo convolucional cumple la cota de Singleton generalizada: La distancia de un [n,k,L] − c´ dfree (C ) ≤ (n − k)
δ k
+ 1 + δ + 1
donde δ es lo que se llama grado externo del código convolucional. [14] El grado externo de un código convolucional es la suma de los grados máximos de las filas en la aplicación que relaciona una palabra de la entrada con una de la salida para cada momento de la codificación: G(z ) : F2 [z]k → F2 [z]n En los códigos con k = 1, el grado externo coincide con la memoria del código, ya que la matriz de la aplicación G tiene dimensiones k × n. Así, para un código C con k = 1, n = 2 y L = 3, δ = L − 1 = 2, y: 19
dfree (C ) ≤ (2 − 1)
2 1
+ 1 + 2 + 1 = 5
Y dado que hemos visto antes que dfree (C ) = 5, podemos concluir que el código tiene la distancia libre máxima para un código con esos parámetros. Este tipo de códigos se llaman MDS (Maximum-distance separable ). A continuación se incluye una imagen con códigos de distancia máxima para n = 2 y distintos valores de L.[1] L g1 g2 dfree 3 5 7 5 4 15 17 6 5 23 35 7 6 53 75 8 7 133 171 10 8 247 371 10 9 561 753 12 10 1167 1545 12 11 2335 3661 14 12 4335 5723 15 13 10533 17661 16 14 21675 27123 16 Se puede ver que una mayor memoria aumenta la distancia libre, pero eso tiene un coste computacional mayor y no es posible aumentarla sin límite. Un mayor n hace la distancia libre mayor, aunque la tasa de información será menor y habrá que enviar un número mayor de bits. Para el cálculo de la distancia libre en códigos con k = 2 es posible utilizar el algoritmo de Viterbi. El algoritmo sería similar al usado en el caso de zero-tailing , que se utilizará para una entrada de ceros con el añadido de que el resultado no sea cero.[5] Para conseguir esto basta con obligar a que la primera transición sea un uno. Dado que se comienza en el estado cero, si esta fuera cero seguiría en el mismo estado y la salida sería de n ceros. Para que la salida del camino completo no sea cero en algún momento tiene que haber una primera transición correspondiente a la entrada de un uno. Como todas las anteriores no aportarían nada al peso de la palabra se puede tomar esta como la primera.
20
6.
Capacidad Correctora.
Determinar la capacidad correctora de un decodificador convolucional es complejo [8], por lo que en este apartado solo se verán algunos ejemplos y se darán algunas cotas que permitan aproximar la probabilidad de recibir un mensaje correctamente. Se podrán detectar errores en un mensaje recibido si el digrama sigue un camino prohibido. Es decir, si en el resultado del algoritmo de Viterbi no tenemos un camino cuya distancia al recibido es cero (valor de la métrica de camino o pathmetric). El error se podrá corregir si el camino ofrecido por el algoritmo de Viterbi coincide con el original. Si el número de errores es demasiado grande no será posible encontrar este camino y se llegará a un camino erroneo, con lo que no se podrán corregir correctamente los errores. Para estimar la probabilidad de errores es posible analizar un mensaje recibido en cadenas de nL bits, donde L es la restricción de distancia y n la longitud de la palabra del código. De forma similar a la que se vio para los códigos de bloques, en el caso de los convolucionales se tiene que d 1 será posible la correción, en cada tira de nL bits, de un máximo de t errores si t ≤ free . 2
−
Sin embargo, no siempre se podrán corregir t errores, dado que esto depende de si la decodificación se realiza para un número suficientemente grande de bits y de la distribución de los errores. Este dato es solo una cota y en otras ocasiones este número será menor, pero en general se puede decir que corregirá t errores si estos se encuentran a la suficiente distancia [4]. Esta no es la mejor forma de medir la capacidad correctora de un código pero puede darnos una estimación. A continuación se muestran algunos casos en los que es posible, o no, corregir un mensaje. Usando el código de ejemplos anteriores se tiene que nL = 6 y se podrían corrige hasta t errores si t ≤ 2 y estos se encuentran lo suficientemente separados. Vemos un ejemplo en el que se envía el mensaje 101011001100 y hay tres errores muy próximos:
21
Como se ve, el mensaje no se ha decodificado correctamente. El diagrama de Trellis es el siguiente:
Se supera la cota vista y el camino rojo tiene la misma distancia de Hamming al recibido que el correcto. Dado que el algoritmo uno cualquiera entre ellos y hay dos posibles, no podemos tener la certeza de cuales fueron las palabras enviadas. En esta otra ocasión, enviando 101011001100100 se producen 5 errores pero están todos lo suficientemente separados como para que el decodificador los corrija.
Por último, un caso en el que no hay más de 2 errores en una secuencia de 6 bits, pero sin embargo no es posible corregirlos debido a su distribución. 22
En este caso el camino más probable no es el correcto sino el marcado en rojo. Las dos lineas azules tienen igual probabilidad, por lo que ambas podrían haber sido tomadas, aunque en este caso ese camino sea cortado más adelante. A continuación vamos a comparar varios códigos convolucionales con otros códigos en mensajes más largos (de N bits) para un canal con probabilidad de error p. De este canal vamos a suponer que los errores se producen de manera aleatoria y que no se producen ráfagas. Esto se da en canales con una probabilidad de error baja, en este caso se ha probado hasta p = 10 1 . Además el mensaje tendrá un largo suficiente y la decodificación se producirá utilizando un número suficiente de bits. −
En un supuesto como este el número de errores que se puede corregir se aproximará a t. Teniendo esto en cuenta calculamos la probabilidad de que una secuencia de n · L llegue correctamente de modo similar a como se haría con los códigos de blques. Esto es, la probabilidad de que no haya errores más la probabilidad de que haya un número de errores que pueda corregir. La probabilidad de que en n · L haya t errores es
n·L t
pk (1 − p)(n L) k . ·
−
Entonces, la probabilidad de que un bloque de n · L llegue correctamente con un código capaz de corregir t errores es:
n·L = p (1 − p) t
pB
k=1
t
t
(n·L)−t
23
Con esto podemos hacer una estimación para un mensaje de N bits. La probabilidad de que llegue correcto es: pM ≥ ( pB )N n ∗
Es decir, la probabilidad de ir desplazando una secuencia de n · L bits por cada uno de los bits codificados. A continuación se muestra una tabla para N = 10000 bits y distintos códigos con la probabilidad de que llegue correcto. Para los códigos convolucionales se muestra la estimación de la cota inferior vista y entre paréntesis el resultlado de una simulación para 500 transmisiones: p = 0,001 p = 0,01 bits enviados 0 0 10000 Sin código 0,998 (100 %) 0,676(72 %) 20000 Convolucional L = 3,n = 2 - (5, 7) 1 (100 %) 0,998 (100 % ) 30000 Convolucional L = 4,n = 3 - (11, 15, 17) 1 (100 %) 0,996 (100 % ) 20000 Convolucional L = 7,n = 2 - (115, 147) 0,97 0,05 30000 Triple Repetición 0,94 0,006 17500 Hamming(3) 0,91 0,00015 10714 Hammin(4) A partir de estos datos podemos ver que los códigos convolucionales son una buena opción en canales con mucha probabilidad de error , ya que son capaces de corregir grandes cantidades de errores sin necesidad de enviar muchos bits redundantes. En las capturas siguientes es posible ver la diferencia entre utilizar un código Hamming y un código convolucional con memoria 3. Enviando un 14 % más de bits es capaz de decodificar el código en un número mucho mayor de ocasiones. Utilizando otro de los códigos convolucionales vistos en la tabla, esta mejora sería aún mayor.
24
25
7.
Mejoras de los Códigos Convolucionales
En esta sección se muestran algunas de las mejoras que se pueden hacer sobre los códigos convolucionales y formas de usarlos para obtener un mayor rendimiento. Codificadores recursivos
Existen codificadores recursivos. Son aquellos en los que a la entrada se le suma alguna de las sumas anteriores como en el siguiente ejemplo.
Es posible utilizar un codificador de este tipo para conseguir resultados como los de un codificador no recursivo pero utilizando una circuitería más sencilla. Este codificador, además, es sistemático. Esto significa que la entrada está incluida en la salida (salida X 2 ), generalmente los codificadores recursivos son sistematicos mientras que los no-recursivos no lo son. Por lo general un código sistemático tiene una menor distancia libre que uno no-sistemático. Códigos catastróficos.
Son aquellos cuyo diagrama de estados contiene un ciclo en el que para una secuencia que no sea de ceros tiene una salida completa de ceros. Esto supone que entradas que difieren en un número infinito de bits pueden producir salidas que difieran en un número finito pequeño de ellos. Este tipo de códigos se debe evitar ya que un número pequeño de errores puede producir un gran número de errores después de codificar. Solamente 1/(2n − >) de los códigos con ratio 1/n son catastróficos, sin embargo es necesario prestar atención para evitarlos y un conjunto de condiciones suficientes para evitarlo han sido probadas [11].
26
Para este código la entrada 000 . . . 000 produciría una salida de ceros, y la entrada 111 . . . 111 tiene como salida 1001000 . . . 000. De este modo, con que se produjeran solo dos errores en la codificación de una palabra tan larga como quisiéramos, la decodificación sería incorrecta. Los códigos sistemáticos nombrados en el apartado anterior no son, en ningún caso, códigos catastróficos. Códigos perforados
Mediante esta técnica se eliminan algunos de los bits de salida de forma que no se envíen el canal. Con esto se consigue un código con un ratio mayor, aunque la capacidad de corrección sea menor. Se puede definir un código perforado por una matriz cuyos elementos son 1 si se debe enviar el bit correspondiente o 0 en caso contrario. Un ejemplo es la matriz siguiente, que genera un código con ratio 2/3 a partir de uno con ratio 1/2. P =
1 0 1 1
El número de filas es el número de polinomios generadores. La matriz se aplicará de forma cíclica. De este modo se indica que del primer polinomio se enviará el bit resultado solo una de cada dos veces. Posteriormente el decodificador de Viterbi ignorará estos bits. Mejora ante rachas de errores
Los códigos convolucionales con k = 1 y memoria pequeña, tal como hemos visto, no permiten la corrección de largas rachas de errores (ráfagas). Para evitar esto se suelen utilizar en conjunto con otros códigos. Una de las opciones mas comunes es hacerlo junto a códigos de Reed-Solomon. La técnica habitual consiste en una codificación con un código RS, posteriormente un entrelazado y finalmente la codificación convolucional. El mismo proceso se realizará al contrario en el decodificador. De este modo se consiguen códigos con capacidad de corregir un gran número de errores gracias al código convolucional, y aún cuando esto se encuentren por rachas, gracias a los códigos RS y el entrelazado. Tubo códigos
Un camino a seguir después de los códigos convolucionales es el de los turbo códigos. Estos hacen uso de varios códigos combinados y consiguen alcanzar la capacidad correctora de códigos convolucionales con memorias mayores sin que su decodificación suponga una comple jidad equivalente a la de los convolucionales. Un caso simple es en el que se hacen varias copias de la entrada y mientras que una copia se envía por un código convolucional, otra se permuta y se envía por uno distinto. 27
8.
Aplicaciones
Estos códigos, teniendo en cuenta las mejoras vistas, son especialmente útiles en aquellos canales con una probablidad de error muy alta . Usados en conjunto con otros códigos serían muy adecuados también en los que por sus carácteristas se producen ráfagas de errores. Por esto junto a otro código se usan en la televisión digital DVB (por cable, DVB-C; satélite, DVB-S; o terrestre, DVB-T) y en telefonía movil GSM. En concreto, se utiliza un código convolucional con parámetros L = 5, k = 1, n = 2. También encontramos estos códigos los antiguos modems telefónicos o los estándares actuales ADSL2+, o en transmisiones en el espacio con sondas o en la dirección de misiones no tripuladas. El uso de códigos convolucionales ha tenido un gran peso en la exploración espacial. Anteriormente a 1969 no se utilizaban códigos en las transmisiones y antes de utilizar códigos convolucionales se utilizó un código llamado código biortogonal de bloques. A partir de 1977 se comenzó a utilizar un código convolucional con decodificación por el algoritmo de Viterbi. Un ejemplo del uso de este código fue en las misiones de ambas Voyager donde se utilizaron códigos convolucionales de parámetros L = 7, k = 1, n = 2. La Voyager 1 actualmente se encuentra en el límite del sistema solar y aún hoy se siguen recibiendo datos. En 1986 se empezaron a utilizar códigos con mayor memoria, en concreto con L = 15 y su combinación con códigos RS. Se han usado, por ejemplo, en la sonda Cassini para la exploración de Saturno o en los rovers enviados a Marte. Una muestra de la mejora que han supuesto estos códigos en la exploración espacial se puede ver en la siguiente imagen. En el eje horizontal se muestra lo que se llama SNR por bit, utilizada para medir el rendimiento del código para cada probabilidad de error del canal en el eje vertical.
Más información sobre los códigos de la exploración espacial se puede encontrar en [9].
28
Anexo A. Programa de Simulación Codice.
Junto al trabajo se ha desarrollado esta aplicación que permite simular códigos Convolucionales con k = 1. Se ha añadido además la posibilidad de utilizar códigos de Hamming o mensajes sin codificar para compararlos. No se ha puesto especial atención al rendimiento o al diseño del software. Sin embargo ha sido útil para mostrar ejemplos, estudiar las probabilidades de distintos códigos y crear los diagramas de Trellis de los códigos convolucionales.
Uso del programa
La ventana que se muestra al abrir la aplicación se divide en tres partes. En la parte izquierda es posible elegir el código y las propiedades así como la probabilidad de error de canal. Tras una simulación también se mostrará en esta zona, en la parte inferior la distancia calculada para el código, la dimensión y la longitud. La forma de introducir los polinomios de un código convolucional es poniendo los coeficientes de menor a mayor grado y separar cada uno mediante punto y coma (;) respetando los espacios de la forma siguiente: a0 , a1,...,am ; b0 , b1 ,...,bm ...
29
En el centro de la ventana hay cuatro cajas de texto que permiten visualizar el proceso de la transmisión. En la primera de ellas se podrá introducir el mensaje que se desea enviar. Este puede ser en binario o texto (ASCII), y la opción se debe elegir en la parte superior. También es posible generar un mensaje aleatorio de un número determinado de bits mediante la opción correspondiente. Una vez escrito el mensaje, al pulsar la tecla Simular, aparecerá en el resto de las cajas de texto, por este orden, el mensaje codificado, el mensaje recibido y por último el mensaje decodificado con el número de errores que haya sido posible corregir corregidos. En los dos últimos aparecerán en verde los bits o símbolos correctos y en rojo los incorrectos. En los mensajes codificados se muestra una palabra por fila para una visualización más sencilla.
Por último, a la derecha aparecerá el número de errores en el mensaje recibido (er) y el número de mensajes en el código decodificado (ef ). Más abajo se puede ver una estimación de la probabilidad de que el mensaje llegue correctamente (P b, Probabilidad de mensaje bueno). En el caso de los códigos convolucionales esta probabilidad será una cota inferior teniendo en cuenta que el mensaje tenga una longitud suficiente y el canal no tenga ráfagas.
Detalles de implementación
Para realizar el programa se ha utilizado el lenguaje de programación C++. La interfaz gráfica se ha hecho utilizando las librerías Qt por ser multiplataforma. Para las operaciones con 30
matrices se utiliza la librería Armadillo que permite hacer productos de matrices, modificaciones, etc. de forma sencilla y eficiente. En el caso de los códigos de Hamming es necesario crear las matrices generadora y de control. En primer lugar se crea la matriz de control y se permutan sus columnas de forma que aparezca la identidad. A partir de esta se crea la generadora de forma trivial. La codificación consistirá en el producto de una matriz con las palabras a codificar por la generadora y la decodificación se hace mediante síndromes. Para esto último se mantendrá una lista de síndromes con su error asociado con la que se comparara en la decodificación de cada palabra. En el caso de los códigos convolucionales se utiliza el enfoque similar a la visión mediante registros hardware vista. Se creará una matriz de elementos binarios con los coeficientes de los polinomios generadores y otra en la que para cada bit se tendrá una fila formada por ese bit y los m anteriores. De este modo la codificación consistiría en el producto de esta matriz por la generadora. Una implementación más eficiente de esto se podría realizar utilizando lo visto sobre codificación en el apartado correspondiente de este trabajo. La decodificación y el cálculo de la distancia libre se hacen mediante el algoritmo de Viterbi tal como está explicado en el pseudocódigo mostrado en el trabajo.
31