11. Fichiers IQ et SigMF¶

Dans tous nos exemples Python prĂ©cĂ©dents, nous avons stockĂ© les signaux sous forme de tableaux NumPy 1D de type “flottants complexes”. Dans ce chapitre, nous apprenons comment les signaux peuvent ĂȘtre stockĂ©s dans un fichier, puis relus dans Python, et nous prĂ©sentons la norme SigMF. Le stockage des donnĂ©es de signaux dans un fichier est extrĂȘmement utile ; vous pouvez souhaiter enregistrer un signal dans un fichier afin de l’analyser manuellement hors ligne, de le partager avec un collĂšgue ou de constituer un ensemble de donnĂ©es complet.

Fichiers binaires¶

Rappelons qu’un signal numĂ©rique en bande de base est une sĂ©quence de nombres complexes.

Exemple : [0.123 + j0.512, 0.0312 + j0.4123, 0.1423 + j0.06512, 
]

Ces nombres correspondent à [I+jQ, I+jQ, I+jQ, I+jQ, I+jQ, I+jQ, I+jQ, I+jQ, 
].

Lorsque nous voulons enregistrer des nombres complexes dans un fichier, nous les enregistrons au format IQIQIQIQIQIQIQIQ. C’est-Ă -dire que nous stockons un tas de flottants dans une rangĂ©e, et lorsque nous les relisons, nous devons les sĂ©parer en [I+jQ, I+jQ, 
].

Bien qu’il soit possible de stocker les nombres complexes dans un fichier texte ou un fichier csv, nous prĂ©fĂ©rons les enregistrer dans ce que l’on appelle un “fichier binaire” pour gagner de l’espace. À des taux d’échantillonnage Ă©levĂ©s, vos enregistrements de signaux peuvent facilement atteindre plusieurs Go, et nous voulons ĂȘtre aussi Ă©conomes en mĂ©moire que possible. Si vous avez dĂ©jĂ  ouvert un fichier dans un Ă©diteur de texte et qu’il semblait incomprĂ©hensible comme la capture d’écran ci-dessous, il s’agissait probablement d’un fichier binaire. Les fichiers binaires contiennent une sĂ©rie d’octets, et vous devez garder la trace du format vous-mĂȘme. Les fichiers binaires sont le moyen le plus efficace de stocker des donnĂ©es, en supposant que toutes les compressions possibles ont Ă©tĂ© effectuĂ©es. Étant donnĂ© que nos signaux ressemblent gĂ©nĂ©ralement Ă  une sĂ©quence alĂ©atoire de flottants, nous n’essayons gĂ©nĂ©ralement pas de compresser les donnĂ©es. Les fichiers binaires sont utilisĂ©s pour beaucoup d’autres choses, par exemple pour les programmes compilĂ©s (appelĂ©s “binaires”). Lorsqu’ils sont utilisĂ©s pour enregistrer des signaux, nous les appelons “fichiers IQ” binaires, en utilisant l’extension de fichier .iq.

../_images/binary_file.png

En Python, le type complexe par dĂ©faut est np.complex128, qui utilise deux flottants de 64 bits par Ă©chantillon. Mais en DSP/SDR, nous avons tendance Ă  utiliser des flottants de 32 bits Ă  la place, car les ADC de nos SDR n’ont pas tant de prĂ©cision que cela pour justifier des flottants de 64 bits. En Python, nous utiliserons np.complex64, qui utilise deux flottants de 32 bits. Lorsque vous traitez simplement un signal en Python, cela n’a pas vraiment d’importance, mais lorsque vous allez enregistrer le tableau 1d dans un fichier, vous voulez d’abord vous assurer qu’il s’agit d’un tableau de np.complex64.

Exemples Python¶

En Python, et spĂ©cifiquement en numpy, nous utilisons la fonction tofile() pour enregistrer un tableau numpy dans un fichier. Voici un court exemple de crĂ©ation d’un simple signal BPSK plus du bruit et de son enregistrement dans un fichier dans le mĂȘme rĂ©pertoire que celui Ă  partir duquel nous avons exĂ©cutĂ© notre script : .. code-block:: python

import numpy as np import matplotlib.pyplot as plt

num_symbols = 10000

x_symbols = np.random.randint(0, 2, num_symbols)*2-1 # -1 et 1 n = (np.random.randn(num_symbols) + 1j*np.random.randn(num_symbols))/np.sqrt(2) # AWGN de puissance unitaire r = x_symbols + n * np.sqrt(0.01) # puissance du bruit de 0.01 print(r) plt.plot(np.real(r), np.imag(r), ‘.’) plt.grid(True) plt.show()

# Sauvegarder le fichier IQ print(type(r[0])) # VĂ©rifier le type de donnĂ©es. Oups, c’est 128 et non 64 ! r = r.astype(np.complex64) # Convertir en64 print(type(r[0])) # Verifier que c’est bien 64 r.tofile(‘bpsk_in_noise.iq’) # Sauvegarder le fichier

Maintenant, examinez les dĂ©tails du fichier produit et vĂ©rifiez combien d’octets il contient. Ce devrait ĂȘtre num_symbols * 8 parce que nous avons utilisĂ© np.complex64, ce qui reprĂ©sente 8 octets par Ă©chantillon, 4 octets par flottant (2 flottants par Ă©chantillon).

En utilisant un nouveau script Python, nous pouvons lire ce fichier en utilisant np.fromfile(), comme ceci :

import numpy as np
import matplotlib.pyplot as plt

samples = np.fromfile('bpsk_in_noise.iq', np.complex64) # Lire dans le fichier.  Nous devons lui dire quel est son format
print(samples)

# Tracez la constellation pour vous assurer qu'elle est correcte
plt.plot(np.real(samples), np.imag(samples), '.')
plt.grid(True)
plt.show()

Une grosse erreur est d’oublier d’indiquer Ă  np.fromfile() le format du fichier. Les fichiers binaires n’incluent aucune information sur leur format. Par dĂ©faut, np.fromfile() suppose qu’il lit un tableau de float64s.

La plupart des autres langages ont des mĂ©thodes pour lire les fichiers binaires, par exemple, dans MATLAB vous pouvez utiliser fread(). Pour l’analyse visuelle d’un fichier RF, voir la section ci-dessous.

Si vous vous retrouvez un jour Ă  traiter des int16 (alias ints courts), ou tout autre type de donnĂ©es pour lequel numpy n’a pas d’équivalent complexe, vous serez obligĂ© de lire les Ă©chantillons en tant que rĂ©els, mĂȘme s’ils sont en fait complexes. L’astuce est de les lire en tant que rĂ©els, mais ensuite de les entrelacer dans le format IQIQIQ
 vous-mĂȘme, quelques maniĂšres diffĂ©rentes de le faire sont montrĂ©es ci-dessous :

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 # convertir en -1 en +1 (facultatif)
samples = samples[::2] + 1j*samples[1::2] # convertir en IQIQIQ...

Analyse visuelle d’un fichier RF¶

Bien que nous ayons appris Ă  crĂ©er notre propre tracĂ© de spectrogramme dans le chapitre Domaine frĂ©quentiel, rien ne vaut l’utilisation d’un logiciel dĂ©jĂ  crĂ©Ă©, et quand il s’agit d’analyser un long enregistrement RF, je recommande d’utiliser inspectrum. Inspectrum est un outil graphique assez simple mais puissant pour balayer visuellement un fichier RF, avec un contrĂŽle fin sur la gamme de cartes de couleurs et la taille de la FFT (quantitĂ© de zoom). Vous pouvez maintenir la touche alt et utiliser la molette de dĂ©filement pour vous dĂ©placer dans le temps. Il dispose de curseurs optionnels pour mesurer le delta-temps entre deux rafales d’énergie, et la possibilitĂ© d’exporter une tranche du fichier RF dans un nouveau fichier. Pour l’installation sur des plateformes basĂ©es sur Debian comme Ubuntu, utilisez les commandes suivantes :

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

Valeurs maximales et saturation¶

Lorsque vous recevez des Ă©chantillons d’un SDR, il est important de connaĂźtre la valeur maximale de l’échantillon. De nombreux SDR Ă©mettent les Ă©chantillons sous forme de flottants avec une valeur maximale de 1.0 et une valeur minimale de -1.0. D’autres SDR vous donneront des Ă©chantillons sous forme d’entiers, gĂ©nĂ©ralement 16 bits, auquel cas les valeurs max et min seront +32767 et -32768 (sauf indication contraire), et vous pouvez choisir de diviser par 32 768 pour les convertir en flottants de -1,0 Ă  1,0. La raison pour laquelle il faut connaĂźtre la valeur maximale de votre SDR est due Ă  la saturation : lors de la rĂ©ception d’un signal extrĂȘmement fort (ou si le gain est rĂ©glĂ© trop haut), le rĂ©cepteur va “saturer” et il va tronquer les valeurs Ă©levĂ©es Ă  la valeur maximale de l’échantillon. Les ADCs de nos SDRs ont un nombre limitĂ© de bits. Lorsque vous crĂ©ez une application SDR, il est sage de toujours vĂ©rifier la saturation, et lorsque cela se produit, vous devez l’indiquer d’une maniĂšre ou d’une autre. Un signal qui est saturĂ© aura l’air perturbĂ© dans le domaine temporel, comme ceci :

../_images/saturated_time.png

En raison des changements soudains dans le domaine temporel, dus Ă  la troncature, le domaine frĂ©quentiel peut sembler Ă©talĂ©. En d’autres termes, le domaine des frĂ©quences comprendra de fausses caractĂ©ristiques, des caractĂ©ristiques rĂ©sultant de la saturation et ne faisant pas rĂ©ellement partie du signal, ce qui peut dĂ©concerter les gens lors de l’analyse d’un signal.

SigMF et l’annotation des fichiers IQ¶

Comme le fichier IQ lui-mĂȘme n’est associĂ© Ă  aucune mĂ©tadonnĂ©e, il est courant d’avoir un second fichier contenant des informations sur le signal, portant le mĂȘme nom de fichier mais une extension .txt ou autre. Ces informations devraient au minimum inclure la frĂ©quence d’échantillonnage utilisĂ©e pour collecter le signal, et la frĂ©quence sur laquelle le SDR Ă©tait accordĂ©. AprĂšs l’analyse du signal, le fichier de mĂ©tadonnĂ©es peut inclure des informations sur les plages d’échantillonnage des caractĂ©ristiques intĂ©ressantes, telles que les rafales d’énergie. L’index d’échantillon est simplement un nombre entier qui commence Ă  0 et s’incrĂ©mente Ă  chaque Ă©chantillon complexe. Si vous savez qu’il y a de l’énergie entre les Ă©chantillons 492342 et 528492, vous pouvez lire le fichier et extraire cette partie du tableau : samples[492342:528493].

Heureusement, il existe dĂ©sormais une norme ouverte qui spĂ©cifie un format de mĂ©tadonnĂ©es utilisĂ© pour dĂ©crire les enregistrements de signaux, connue sous le nom de SigMF. En utilisant une norme ouverte comme SigMF, de multiples parties peuvent partager des enregistrements RF plus facilement, et utiliser diffĂ©rents outils pour opĂ©rer sur les mĂȘmes ensembles de donnĂ©es. Cela permet Ă©galement d’éviter le “bitrot” des ensembles de donnĂ©es RF oĂč les dĂ©tails de la capture sont perdus au fil du temps en raison de dĂ©tails de l’enregistrement qui ne sont pas colocalisĂ©s avec l’enregistrement lui-mĂȘme. La façon la plus simple (et minimale) d’utiliser le standard SigMF pour dĂ©crire un fichier IQ binaire que vous avez crĂ©Ă© est de renommer le fichier .iq en .sigmf-data et de crĂ©er un nouveau fichier avec le mĂȘme nom mais l’extension .sigmf-meta, et de s’assurer que le champ datatype dans le mĂ©ta-fichier correspond au format binaire de votre fichier de donnĂ©es. Ce fichier mĂ©ta est un fichier en texte clair rempli de json, vous pouvez donc simplement l’ouvrir avec un Ă©diteur de texte et le remplir manuellement (nous verrons plus tard comment le faire de maniĂšre automatique). Voici un exemple de fichier .sigmf-meta que vous pouvez utiliser comme modĂšle :

{
    "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": []
}

Notez que core:cf32_le indique que votre fichier .sigmf-data est de type IQIQIQIQ
 avec des flottants 32 bits, c’est-Ă -dire np.complex64 comme nous l’avons utilisĂ© prĂ©cĂ©demment. RĂ©fĂ©rez-vous aux spĂ©cifications pour les autres types de donnĂ©es disponibles, par exemple si vous avez des donnĂ©es rĂ©elles au lieu de complexes, ou si vous utilisez des entiers 16 bits au lieu de flottants pour gagner de la place.

En dehors du type de donnĂ©es, les lignes les plus importantes Ă  remplir sont core:sample_rate et core:frequency. Il est bon de saisir Ă©galement des informations sur le matĂ©riel (core:hw) utilisĂ© pour capturer l’enregistrement, comme le type de SDR et l’antenne, ainsi qu’une description de ce que l’on sait du ou des signaux dans l’enregistrement dans core:description. Le core:version est simplement la version de la norme SigMF utilisĂ©e au moment de la crĂ©ation du fichier de mĂ©tadonnĂ©es.

Si vous capturez votre enregistrement RF Ă  partir de Python, par exemple en utilisant l’API Python pour votre SDR, vous pouvez Ă©viter de devoir crĂ©er manuellement ces fichiers de mĂ©tadonnĂ©es en utilisant le paquetage SigMF Python. Celui-ci peut ĂȘtre installĂ© sur un systĂšme d’exploitation basĂ© sur Ubuntu/Debian comme suit :

pip install sigmf

Le code Python permettant d’écrire le fichier .sigmf-meta pour l’exemple du dĂ©but de ce chapitre, oĂč nous avons enregistrĂ© bpsk_in_noise.iq, est prĂ©sentĂ© ci-dessous :

import datetime as dt

import numpy as np
import sigmf
from sigmf import SigMFFile

# <code pour exemple>

# r.tofile('bpsk_in_noise.iq')
r.tofile('bpsk_in_noise.sigmf-data') # remplacer la ligne ci-dessus par celle-ci

# crérer les metadata
meta = SigMFFile(
    data_file='bpsk_in_noise.sigmf-data', # extension optionalle
    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__,
    }
)

# créer une clé de capture à l'index temporel 0
meta.add_capture(0, metadata={
    SigMFFile.FREQUENCY_KEY: 915000000,
    SigMFFile.DATETIME_KEY: dt.datetime.now(dt.timezone.utc).isoformat(),
})

# vérifier les erreurs et écrire sur le disque
assert meta.validate()
meta.tofile('bpsk_in_noise.sigmf-meta') # extension optionalle

Remplacez simplement 8000000 et 915000000 par les variables que vous avez utilisĂ©es pour stocker respectivement la frĂ©quence d’échantillonnage et la frĂ©quence centrale.

Pour lire un enregistrement SigMF dans Python, utilisez le code suivant. Dans cet exemple, les deux fichiers SigMF doivent ĂȘtre nommĂ©s bpsk_in_noise.sigmf-meta et bpsk_in_noise.sigmf-data.

from sigmf import SigMFFile, sigmffile

# charger les données
filename = 'bpsk_in_noise'
signal = sigmffile.fromfile(filename)
samples = signal.read_samples().view(np.complex64).flatten()
print(samples[0:10]) # examinons les 10 premiers Ă©chantillons

# Obtenir certaines métadonnées et toutes les annotations
sample_rate = signal.get_global_field(SigMFFile.SAMPLE_RATE_KEY)
sample_count = signal.sample_count
signal_duration = sample_count / sample_rate

Pour plus de détails, voir la référence the SigMF Python documentation.

Un petit bonus pour ceux qui ont lu jusqu’ici: le logo SigMF est en fait stockĂ© comme un enregistrement SigMF lui-mĂȘme, et quand le signal est tracĂ© comme une constellation (IQ plot) dans le temps, il produit l’animation suivante :

../_images/sigmf_logo.gif

Le code Python utilisé pour lire le fichier du logo (situé ici) et produire le gif animé ci-dessus est présenté ci-dessous, pour les curieux :

from pathlib import Path
from tempfile import TemporaryDirectory

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

# charger les données
filename = 'sigmf_logo' # supposez qu'il se trouve dans le mĂȘme rĂ©pertoire que ce script
signal = sigmffile.fromfile(filename)
samples = signal.read_samples().view(np.complex64).flatten()

# Ajoutez des zéros à la fin pour que ce soit clair lorsque l'animation se répÚte.
samples = np.concatenate((samples, np.zeros(50000)))

sample_count = len(samples)
samples_per_frame = 5000
num_frames = int(sample_count/samples_per_frame)

with TemporaryDirectory() as temp_dir:
   filenames = []
   output_dir = Path(temp_dir)
   for i in range(num_frames):
       print(f"frame {i} out of {num_frames}")
       # tracer le cadre
       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])  # garder les axes existants
       ax.set_facecolor('black')  # couleur d'arriĂšre plan

       # Enregister la figure dans un fichier
       filename = output_dir.joinpath(f"sigmf_logo_{i}.png")
       fig.savefig(filename, bbox_inches='tight')
       plt.close()
       filenames.append(filename)

   # Créer un gif animé
   images = [iio.imread(f) for f in filenames]
   iio.imwrite('sigmf_logo.gif', images, fps=20)