13. Archivos IQ y SigMF

En todos nuestros ejemplos anteriores de Python almacenamos se帽ales como matrices 1D NumPy de tipo 鈥渇lotante complejo鈥. En este cap铆tulo aprenderemos c贸mo se pueden almacenar las se帽ales en un archivo y luego volver a leerlas en Python, adem谩s de presentar el est谩ndar SigMF. Almacenar datos de se帽ales en un archivo es extremadamente 煤til; es posible que desee grabar una se帽al en un archivo para analizarla manualmente sin conexi贸n, compartirla con un colega o crear un conjunto de datos completo.

Archivos Binarios

Recuerde que una se帽al digital en banda base es una secuencia de n煤meros complejos.

Ejemplo: [0,123 + j0,512, 0,0312 + j0,4123, 0,1423 + j0,06512, 鈥

Estos n煤meros corresponden a [I+jQ, I+jQ, I+jQ, I+jQ, I+jQ, I+jQ, I+jQ, 鈥

Cuando queremos guardar n煤meros complejos en un archivo, los guardamos en el formato IQIQIQIQIQIQIQIQ. Es decir, almacenamos un mont贸n de flotantes en una fila, y cuando los volvemos a leer debemos separarlos nuevamente en [I+jQ, I+jQ, 鈥.

Si bien es posible almacenar los n煤meros complejos en un archivo de texto o csv, preferimos guardarlos en lo que se llama un 鈥渁rchivo binario鈥 para ahorrar espacio. Con frecuencias de muestreo altas, sus grabaciones de se帽ales podr铆an f谩cilmente ocupar varios GB y queremos utilizar la memoria lo m谩s eficientemente posible. Si alguna vez abri贸 un archivo en un editor de texto y parec铆a incomprensible como en la captura de pantalla a continuaci贸n, probablemente era binario. Los archivos binarios contienen una serie de bytes y usted mismo debe realizar un seguimiento del formato. Los archivos binarios son la forma m谩s eficiente de almacenar datos, suponiendo que se haya realizado toda la compresi贸n posible. Debido a que nuestras se帽ales generalmente aparecen como una secuencia aleatoria de flotantes, normalmente no intentamos comprimir los datos. Los archivos binarios se utilizan para muchas otras cosas, por ejemplo, programas compilados (llamados 鈥渂inarios鈥). Cuando se utilizan para guardar se帽ales, los llamamos 鈥渁rchivos IQ鈥 binarios y utilizamos la extensi贸n de archivo .iq.

../_images/binary_file.png

En Python, el tipo complejo predeterminado es np.complex128, que utiliza dos flotantes de 64 bits por muestra. Pero en DSP/SDR, tendemos a usar flotantes de 32 bits porque los ADC de nuestros SDR no tienen esa precisi贸n para garantizar flotantes de 64 bits. En Python usaremos np.complex64, que usa dos flotantes de 32 bits. Cuando simplemente est谩s procesando una se帽al en Python, realmente no importa, pero cuando vas a guardar la matriz 1d en un archivo, primero debes asegurarte de que sea una matriz de np.complex64.

Ejemplos en Python

En Python, y numpy espec铆ficamente, usamos la funci贸n tofile() para almacenar una matriz numpy en un archivo. Aqu铆 hay un breve ejemplo de c贸mo crear una se帽al BPSK simple m谩s ruido y guardarla en un archivo en el mismo directorio desde donde ejecutamos nuestro script:

import numpy as np
import matplotlib.pyplot as plt

num_symbols = 10000

x_symbols = np.random.randint(0, 2, num_symbols)*2-1 # -1 and 1's
n = (np.random.randn(num_symbols) + 1j*np.random.randn(num_symbols))/np.sqrt(2) # AWGN with unity power
r = x_symbols + n * np.sqrt(0.01) # noise power of 0.01
print(r)
plt.plot(np.real(r), np.imag(r), '.')
plt.grid(True)
plt.show()

# Now save to an IQ file
print(type(r[0])) # Check data type.  Oops it's 128 not 64!
r = r.astype(np.complex64) # Convert to 64
print(type(r[0])) # Verify it's 64
r.tofile('bpsk_in_noise.iq') # Save to file

Ahora examine los detalles del archivo producido y verifique cu谩ntos bytes tiene. Deber铆a ser num_symbols * 8 porque usamos np.complex64, que son 8 bytes por muestra, 4 bytes por flotante (2 flotantes por muestra).

Usando un nuevo script de Python, podemos leer este archivo usando np.fromfile(), al igual que:

import numpy as np
import matplotlib.pyplot as plt

samples = np.fromfile('bpsk_in_noise.iq', np.complex64) # Read in file.  We have to tell it what format it is
print(samples)

# Plot constellation to make sure it looks right
plt.plot(np.real(samples), np.imag(samples), '.')
plt.grid(True)
plt.show()

Un gran error es olvidar decirle a np.fromfile() el formato del archivo. Los archivos binarios no incluyen ninguna informaci贸n sobre su formato. De forma predeterminada, np.fromfile() asume que est谩 leyendo en una matriz de float64.

La mayor铆a de los otros lenguajes tienen m茅todos para leer archivos binarios, por ejemplo, en MATLAB puedes usar fread(). Para analizar visualmente un archivo RF, consulte la secci贸n siguiente.

Si alguna vez te encuentras tratando con int16 (tambi茅n conocidos como ints cortos), o cualquier otro tipo de datos para el que numpy no tenga un equivalente complejo, te ver谩s obligado a leer las muestras como reales, incluso si en realidad son complejas. El truco consiste en leerlos como reales, pero luego intercalarlos nuevamente en el formato IQIQIQ鈥 usted mismo; a continuaci贸n se muestran un par de formas diferentes de hacerlo:

samples = np.fromfile('iq_samples_as_int16.iq', np.int16).astype(np.float32).view(np.complex64)

or

samples = np.fromfile('iq_samples_as_int16.iq', np.int16)
samples /= 32768 # convert to -1 to +1 (optional)
samples = samples[::2] + 1j*samples[1::2] # convert to IQIQIQ...

An谩lisis visual de un archivo RF

Aunque aprendimos c贸mo crear nuestro propio diagrama de espectrograma en el capitulo Dominio de la Frecuencia , no hay nada mejor que utilizar un software ya creado. Cuando se trata de analizar grabaciones de RF sin tener que instalar nada, el sitio web IQEngine que es un conjunto de herramientas para analizar, procesar y compartir grabaciones de RF.

Para aquellos que quieran una aplicaci贸n de escritorio, tambi茅n existe inspectrum. Inspectrum es una herramienta gr谩fica bastante simple pero poderosa para escanear visualmente un archivo RF, con un control preciso sobre el rango del mapa de colores y el tama帽o FFT (cantidad de zoom). Puede mantener presionada la tecla Alt y usar la rueda de desplazamiento para desplazarse en el tiempo. Tiene cursores opcionales para medir el tiempo delta entre dos r谩fagas de energ铆a y la capacidad de exportar una porci贸n del archivo RF a un archivo nuevo. Para la instalaci贸n en plataformas basadas en Debian como Ubuntu, utilice los siguientes comandos:

sudo apt-get install qt5-default libfftw3-dev cmake pkg-config libliquid-dev
git clone https://github.com/miek/inspectrum.git
cd inspectrum
mkdir build
cd build
cmake ..
make
sudo make install
inspectrum
../_images/inspectrum.jpg

Valores m谩ximos y saturaci贸n

Al recibir muestras de un SDR, es importante conocer el valor m谩ximo de muestra. Muchos SDR generar谩n muestras como flotantes utilizando un valor m谩ximo de 1,0 y un valor m铆nimo de -1,0. Otros SDR le dar谩n muestras como n煤meros enteros, generalmente de 16 bits, en cuyo caso los valores m谩ximo y m铆nimo ser谩n +32767 y -32768 (a menos que se especifique lo contrario), y puede optar por dividirlos entre 32,768 para convertirlos en flotantes desde - 1,0 a 1,0. La raz贸n para estar atento al valor m谩ximo de su SDR se debe a la saturaci贸n: al recibir una se帽al extremadamente alta (o si la ganancia est谩 demasiado alta), el receptor se 鈥渟aturar谩鈥 y truncar谩 los valores altos a cualquiera que sea el valor m谩ximo de muestra. Los ADC de nuestros SDR tienen un n煤mero limitado de bits. Al crear una aplicaci贸n SDR, es aconsejable comprobar siempre la saturaci贸n y, cuando esto suceda, debes indicarlo de alguna manera.

Una se帽al saturada se ver谩 entrecortada en el dominio del tiempo, as铆:

Example of a saturated receiver where the signal is clipped

Debido a los cambios repentinos en el dominio del tiempo, debido al truncamiento, el dominio de la frecuencia puede verse borroso. En otras palabras, el dominio de la frecuencia incluir谩 caracter铆sticas falsas; caracter铆sticas que resultaron de la saturaci贸n y que en realidad no son parte de la se帽al, lo que puede desorientar a las personas al analizar una se帽al.

SigMF y Anotaci贸n de archivos IQ

Dado que el archivo IQ en s铆 no tiene ning煤n metadato asociado, es com煤n tener un segundo archivo que contenga informaci贸n sobre la se帽al, con el mismo nombre de archivo pero con .txt u otra extensi贸n de archivo. Esto debe incluir, como m铆nimo, la frecuencia de muestreo utilizada para recopilar la se帽al y la frecuencia a la que se sintoniz贸 el SDR. Despu茅s de analizar la se帽al, el archivo de metadatos podr铆a incluir informaci贸n sobre rangos de muestra de caracter铆sticas interesantes, como r谩fagas de energ铆a. El 铆ndice de muestra es simplemente un n煤mero entero que comienza en 0 e incrementa cada muestra compleja. Si supiera que hay energ铆a desde la muestra 492342 a la 528492, entonces podr铆a leer el archivo y extraer esa parte de la matriz: samples[492342:528493].

Afortunadamente, ahora existe un est谩ndar abierto que especifica un formato de metadatos utilizado para describir grabaciones de se帽ales, conocido como SigMF. Al utilizar un est谩ndar abierto como SigMF, varias partes pueden compartir grabaciones de RF m谩s f谩cilmente y utilizar diferentes herramientas para operar en los mismos conjuntos de datos, como IQEngine. Tambi茅n evita el 鈥渂itrot鈥 de conjuntos de datos de RF donde los detalles de la captura se pierden con el tiempo debido a que los detalles de la grabaci贸n no se ubican con la grabaci贸n misma.

La forma m谩s sencilla (y m铆nima) de utilizar el est谩ndar SigMF para describir un archivo IQ binario que haya creado es cambiar el nombre del archivo .iq a .sigmf-data y crear un nuevo archivo con el mismo nombre pero con la extensi贸n .sigmf-meta. y aseg煤rese de que el campo de tipo de datos en el metaarchivo coincida con el formato binario de su archivo de datos. Este metaarchivo es un archivo de texto sin formato lleno de json, por lo que simplemente puede abrirlo con un editor de texto y completarlo manualmente (m谩s adelante discutiremos c贸mo hacerlo mediante programaci贸n). A continuaci贸n se muestra un archivo .sigmf-meta de ejemplo que puede utilizar como plantilla:

{
    "global": {
        "core:datatype": "cf32_le",
        "core:sample_rate": 1000000,
        "core:hw": "PlutoSDR with 915 MHz whip antenna",
        "core:author": "Art Vandelay",
        "core:version": "1.0.0"
    },
    "captures": [
        {
            "core:sample_start": 0,
            "core:frequency": 915000000
        }
    ],
    "annotations": []
}

Note que core:cf32_le indica que sus datos .sigmf son del tipo IQIQIQIQ鈥 con flotantes de 32 bits, es decir, np.complex64 como usamos anteriormente. Consulte las especificaciones para otros tipos de datos disponibles, como si tiene datos reales en lugar de complejos o si utiliza enteros de 16 bits en lugar de flotantes para ahorrar espacio.

Aparte del tipo de datos, las l铆neas m谩s importantes a completar son core:sample_rate y core:frequency. Es una buena pr谩ctica introducir tambi茅n informaci贸n sobre el hardware.(core:hw) utilizado para capturar la grabaci贸n, como el tipo SDR y la antena, as铆 como una descripci贸n de lo que se sabe sobre la(s) se帽al(es) en la grabaci贸n en core:description. El core:version es simplemente la versi贸n del est谩ndar SigMF que se utiliza en el momento en que se cre贸 el archivo de metadatos.

Si est谩 capturando su grabaci贸n de RF desde Python, por ejemplo, utilizando la API de Python para su SDR, puede evitar tener que crear manualmente estos archivos de metadatos utilizando el paquete SigMF Python. Esto se puede instalar en un sistema operativo basado en Ubuntu/Debian de la siguiente manera:

cd ~
git clone https://github.com/gnuradio/SigMF.git
cd SigMF
sudo pip install .

El c贸digo Python para escribir el archivo .sigmf-meta para el ejemplo del comienzo de este cap铆tulo, donde guardamos bpsk_in_noise.iq, se muestra a continuaci贸n:

import numpy as np
import datetime as dt
from sigmf import SigMFFile

# <code from example>

# r.tofile('bpsk_in_noise.iq')
r.tofile('bpsk_in_noise.sigmf-data') # replace line above with this one

# create the metadata
meta = SigMFFile(
    data_file='example.sigmf-data', # extension is optional
    global_info = {
        SigMFFile.DATATYPE_KEY: 'cf32_le',
        SigMFFile.SAMPLE_RATE_KEY: 8000000,
        SigMFFile.AUTHOR_KEY: 'Your name and/or email',
        SigMFFile.DESCRIPTION_KEY: 'Simulation of BPSK with noise',
        SigMFFile.VERSION_KEY: sigmf.__version__,
    }
)

# create a capture key at time index 0
meta.add_capture(0, metadata={
    SigMFFile.FREQUENCY_KEY: 915000000,
    SigMFFile.DATETIME_KEY: dt.datetime.utcnow().isoformat()+'Z',
})

# check for mistakes and write to disk
meta.validate()
meta.tofile('bpsk_in_noise.sigmf-meta') # extension is optional

Simplemente reemplace 8000000 y 915000000 con las variables que utiliz贸 para almacenar la frecuencia de muestreo y la frecuencia central respectivamente.

Para leer una grabaci贸n SigMF en Python, utilice el siguiente c贸digo. En este ejemplo, los dos archivos SigMF deben denominarse bpsk_in_noise.sigmf-meta y bpsk_in_noise.sigmf-data.

from sigmf import SigMFFile, sigmffile

# Load a dataset
filename = 'bpsk_in_noise'
signal = sigmffile.fromfile(filename)
samples = signal.read_samples().view(np.complex64).flatten()
print(samples[0:10]) # lets look at the first 10 samples

# Get some metadata and all annotations
sample_rate = signal.get_global_field(SigMFFile.SAMPLE_RATE_KEY)
sample_count = signal.sample_count
signal_duration = sample_count / sample_rate

Para m谩s detalles consulte the SigMF documentation.

Una peque帽a ventaja para quienes hayan le铆do hasta aqu铆; El logotipo de SigMF en realidad se almacena como una grabaci贸n de SigMF y cuando la se帽al se traza como una constelaci贸n (gr谩fico IQ) a lo largo del tiempo, produce la siguiente animaci贸n:

The SigMF logo animation

El c贸digo Python utilizado para leer el archivo del logotipo (ubicado aqui) y produzca el gif animado que se muestra a continuaci贸n, para aquellos curiosos:

import numpy as np
import matplotlib.pyplot as plt
import imageio
from sigmf import SigMFFile, sigmffile

# Load a dataset
filename = 'sigmf_logo' # assume its in the same directory as this script
signal = sigmffile.fromfile(filename)
samples = signal.read_samples().view(np.complex64).flatten()

# Add zeros to the end so its clear when the animation repeats
samples = np.concatenate((samples, np.zeros(50000)))

sample_count = len(samples)
samples_per_frame = 5000
num_frames = int(sample_count/samples_per_frame)
filenames = []
for i in range(num_frames):
    print("frame", i, "out of", num_frames)
    # Plot the frame
    fig, ax = plt.subplots(figsize=(5, 5))
    samples_frame = samples[i*samples_per_frame:(i+1)*samples_per_frame]
    ax.plot(np.real(samples_frame), np.imag(samples_frame), color="cyan", marker=".", linestyle="None", markersize=1)
    ax.axis([-0.35,0.35,-0.35,0.35]) # keep axis constant
    ax.set_facecolor('black') # background color

    # Save the plot to a file
    filename = '/tmp/sigmf_logo_' + str(i) + '.png'
    fig.savefig(filename, bbox_inches='tight')
    filenames.append(filename)

# Create animated gif
images = []
for filename in filenames:
    images.append(imageio.imread(filename))
imageio.mimsave('/tmp/sigmf_logo.gif', images, fps=20)