Saltar a contenido

La biblioteca scikit-rf

scikit-rf (conocido como skrf) es un software de código abierto para RF implementado en Python. Proporciona una biblioteca orientada a objetos para el análisis y calibración de redes de radio frecuencia.

Algunas características

  • Lee/escribe archivos Touchstone.
  • Permite operaciones aritméticas sobre parámetros S.
  • Analiza redes en cascada de 2 puertos.
  • Soporta segmentación y concatenación de frecuencias y puertos.
  • Permite la interconexión de redes de N puertos.
  • Convierte entre parámetros S/Z/Y/ABCD/T.
  • Grafica (dB, magnitud, fase, retardo de grupo, diagrama de Smith, etc).

¿Cómo armar un entorno de trabajo sckrf en Windows?

La clase Network()

Una clase es una plantilla que contiene estructura de datos, valores, funciones o métodos. La clase Network() de skrf contiene los atributos y funciones necesarios para trabajar con redes de RF. El primer paso para trabajar, es ''instanciar'' (crear un objeto a partir de una clase) la clase Network() para construir un objeto. Por ejemplo, la información que puede definir a una red puede ser descrita por un archivo Touchstone. Pero también admite su creación a partir de otros parámetros.

¿Qué es un archivo Touchstone?

Creación de un objeto Network() desde archivos Touchstone

Los datos de Touchstone suelen estar disponibles en los softwares de simulaciones de RF o como resultado de las mediciones. Por ejemplo, el archivo bp401.s2p se obtuvo al analizar un filtro con la aplicación NanoVNA-App. Para crear un objeto de tipo Network() simplemente se invoca a Network() y se le otorga la ruta al archivo Touchstone como se indica en el siguiente ejemplo:

(rf) D:\>ipython
Python 3.12.7 | packaged by Anaconda, Inc. | (main, Oct  4 2024, 13:17:27) [MSC v.1929 64 bit (AMD64)]
Type 'copyright', 'credits' or 'license' for more information
IPython 8.27.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: import skrf as rf

In [2]: netrf1= rf.Network("bp401.s2p")

In [3]: print(netrf1)
2-Port Network: 'bp401',  401000000.0-470000000.0 Hz, 401 pts, z0=[50.+0.j 50.+0.j]

In [4]: netrf1.plot_s_db()
mathFunctions.py:268: RuntimeWarning: divide by zero encountered in log10
  out = 20 * np.log10(z)

In [5]: import matplotlib.pyplot as plt

In [6]: plt.show()

La ruta al archivo indicada en In [2] es a un archivo de extensión .s2p donde 2 hace referencia a la cantidad de puertos de la red. Este archivo, se pasa como argumento en la creación del objeto de la clase Network() (en el ejemplo se encuentra en el mismo subdirectorio desde dónde se ejecutó ipython). Cada objeto de la clase Network() tiene un nombre (en este caso netrf1) asociado para una identificación del conjunto de datos.

2-Port Network: 'bp401',  401000000.0-470000000.0 Hz, 401 pts, z0=[50.+0.j 50.+0.j]

La salida ante In [3], indica que fue creado el objeto, que posee dos puertos, su nombre es bp401, tomado por defecto a a partir del nombre del archivo sin su extensión.

401000000.0-470000000.0 Hz indica la frecuencia mínima y máxima de los \(401\) puntos. Por último la impedancia característica en cada uno de sus dos puertos.

Las dos últimas líneas In [5] y In [6] son necesarias para forzar la salida de la imagen en pantalla. Ya que en este caso In [4] no proporcionará salida por si sola. No serán necesarias si utiliza otros intérpretes o entornos cómo Spyder o Jupiter, que escapan al presente apartado. Omitiendo el warning de división por cero, debería obtenerse la siguiente gráfica.

image

El gráfico muestra, el módulo de los 4 parámetros s expresados en dB. Se observa la respuesta de un filtro pasabanda, denota esto \(s_{21}\).

Puede leerse de la siguiente forma:

  • Para frecuencias por debajo a \(4,3 \times 10^{8}\:Hz\), lo que se transfiere desde el puerto 1 al puerto 2 es despreciable, a lo sumo \(10^ \frac{-20}{20}=0,1=10\%\)
  • Entre \(4,3\times 10^{8}\:Hz\) y \(4,4\times 10^{8}\:Hz\), se transfiere prácticamente todo \(10^ \frac{0}{20}=1=100\%\)
  • Finalmente, a partir de \(4,4 \times 10^{8}\) \(Hz\), nuevamente a lo sumo un \(10\%\).

Observar que \(s_{12}\) y \(s_{22}\) no poseen traza (es decir\(-\infty\:dB\)) esto se debe a que NanoVNA posee un único puerto excitador, y este es el puerto 1. (ver NanoVNA)

Es esperable que este cuadripolo, que es un filtro pasivo, sea simétrico (\(s_{11}=s_{22}\)) y recíproco (\(s_{21}=s_{12}\)). Es decir, que la red se comporte de igual manera, tanto si se inyecta señal por el puerto 1 cómo si se lo hace por el puerto 2.

Actividad: Construir una red en base a netrf1 que sea simétrica y recíproca. Almacenarla en netrf1sim.

NOTA: A partir de aquí, por simplicidad se omite el indicador de entrada del intérprete de comandos, así cómo también el mecanismo necesario para forzar la salida de la imagen en pantalla.

Creación de un objeto a partir de matrices NumPy

Con los datos de una red en forma de matrices NumPy, se puede crear un objeto de clase Network().

Para construir una red de dos puertos a partir de matrices NumPy, se necesitan las matrices NumPy complejas. Para este ejemplo, se generaran aleatoriamente. Cada una de estas cantidades tiene una longitud M, correspondiente a M puntos de frecuencia. Por ejemplo se generan 401 puntos de frecuencia equiespaciados entre \(410\:MHz\) y \(470\:MHz\). El código Python para implementarlo es el siguiente:

import numpy as np
import skrf as rf

# Generación de los 401 puntos de frecuencia entre 410 MHz y 470 MHz 
f = np.linspace(410,470,401)*1e6

# Generación de 401 parámetros s y construcción de matriz 401 x 2 x 2
s= np.empty(shape=(401,2,2),dtype=complex)
for i in range(0,401):
   s11 = s22 = np.random.rand() + 1j*np.random.rand()
   s21 = s12 = np.random.rand(401) + 1j*np.random.rand(401)
   s[i] = np.array( [ s11, s12 ] , [ s21, s22 ] )

# creación del objeto "Network"
netrf2 = rf.Network(name='netrf2 creada por numpy',s=s,frequency=f, z0=50)
print(netrf2)
Salida:
2-Port Network: 'netrf2 creada por numpy',  410000000.0-470000000.0 Hz, 401 pts, z0=[50.+0.j 50.+0.j]

Impedancias de referencia

El argumento z0 que se pasa durante la creación del objeto de red es la impedancia de referencia para los parámetros s. Cuando se especifica un único número, todos los puertos, de la red de N puertos, tienen asignada la misma impedancia de referencia z0. Puede haber ocasiones en las que uno o varios puertos tengan diferentes impedancias. Para especificar diferentes impedancias de referencia en cada puerto, z0 también puede haber una lista de impedancias correspondientes a cada puerto como z0=[z01, z01, ..., z0N]. Cada impedancia de referencia puede ser un número real o complejo.

Creación de un objeto desde elementos de circuitos

Media() es la clase base que representa medios de transmisión en único modo, de la cual, se heredarán a instancias de transmisión específicas, como Freespace(), Coaxial() o RectangularWaveguide(). Media() proporciona métodos genéricos para producir líneas de transmisión. Luego, los métodos se heredan las características a métodos específicos, que definen las características relevantes de dicha red. Esto permite que las líneas de transmisión específicas produzcan redes sin tener que volver a implementar métodos para cada instancia. Algunos de los métodos específicos son los siguientes:

  • DefinedGammaZ0(): Un medio definido directamente por su constante de propagación y su impedancia característica.
  • RectangularWaveguide(): Una guía de onda rectangular rellena de forma homogénea.
  • Coaxial(): Una línea de transmisión coaxial definida en términos de sus diámetros interno/externo y su permitividad.
  • MLine(): Una línea de transmisión de microstrip definida en términos de ancho, espesor y altura en un sustrato de permitividad relativa.
  • Freespace(): Onda plana (modo TEM) en el espacio.

Por medio de DefinedGammaZ0() es posible definir redes de circuitos a parámetros concentrados, los parámetros necesarios para su definición son:

  • frequency: Banda de frecuencia. El valor predeterminado produce una banda de \(1\) a \(10\:GHz\) con \(101\) puntos.
  • z0_port: Es la impedancia del puerto.
  • gamma: Es la constante de propagación compleja.
  • z0: impedancia característica compleja del medio. El valor predeterminado es \(50\:\Omega\).

Por ejemplo, para una línea de transmisión sin pérdidas de velocidad de propagación igual a la velocidad de la luz (\(c\)), el parámetro gamma debe ser \(\gamma = \frac {\omega} {c}\)

Luego, distintos métodos instancian redes de dos puertos, con cada puerto conectado a un terminal del elemento, por ejemplo resistor, inductor,capacitor entre otros. Principalmente requieren el valor de resistencia en \(\Omega\), inductancia en \(H\) y capacitancia en \(F\) respectivamente.

Estas redes creadas, deben conectarse de una determinadas forma para conformar una nueva red que describa el comportamiento del conjunto. Para ello con ayuda de la clase Circuit(), se conformara el circuito, aplicando reglas de conexionado, para luego obtener la clase Network() que describe el comportamiento completo de la red.

Los pasos serán los siguientes:

  • 1) ''Instanciar'' un objeto de la clase frequency() con el rango de frecuencias de interés y la cantidad de puntos.
  • 2) ''Instanciar'' un objeto de la clase DefinedGammaZ0() con \(\gamma\) y \(Z_0\) correspondiente.
  • 3) ''Instanciar'' todas las redes de elementos de circuito necesarias con por ejemplo resistor, inductor y capacitor, así como también todos los puertos de conexión al exterior con Port.
  • 4) Declarar una lista de listas de conexiones en cada nodo de circuito.
  • 5) ''Instanciar'' un objeto de la clase Circuit() para crear la topología de conexión.
  • 6) Obtener a partir de aquí la red total.

Modelado de un cristal oscilador de cuarzo

La siguiente imagen muestra el modelado de un cristal oscilador de cuarzo por medio de un equivalente de circuito a parámetros concentrados.

image

Se han puesto en evidencia las redes A, B, C y D que solo contienen un elemento de circuito cada una. En el siguiente ejemplo se desarrolla la construcción en red de la interconexión.

# 1)
freq = rf.Frequency(start=9.984802, stop=10.043330 , unit='MHz', npoints=1601)
# 2)
tl_media = rf.DefinedGammaZ0(freq, z0=50, gamma=1j*freq.w/rf.c)
# 3)
A = tl_media.capacitor(0.016095e-12, name='C1')
B = tl_media.inductor(15.74965e-3, name='L1')
C = tl_media.resistor(9, name='R1')
D = tl_media.capacitor(3.45e-12, name='C2')
Port1 = rf.Circuit.Port(freq, name='Port 1', z0=50)
Port2 = rf.Circuit.Port(freq, name='Port 2', z0=50)
# 4)
cnx = [
    [(Port1, 0), (A, 0), (D, 0)],
    [(A, 1), (B, 0)],
    [(B, 1), (C, 0)],
    [(C, 1), (D, 1), (Port2, 0)],
]
# 5)
cir = rf.Circuit(cnx)
# 6)
netrf3 = cir.network
print(netrf3)

Salida:

2-Port Network: '',  9.984802-10.04333 MHz, 1601 pts, z0=[50.+0.j 50.+0.j]

Manipulación de objetos de red

Para extraer los parámetros s de un puerto como objetos de red de una red de múltiples puertos, puede simplemente acceder al parámetro de red correspondiente como una propiedad como se muestra a continuación:

s11 = netrf1.s11
print(s11)
Salida:
1-Port Network: 'bp401',  401000000.0-470000000.0 Hz, 401 pts, z0=[50.+0.j]
Tener en cuenta que \(s_{11}\) es otro objeto de la clase Network(). Con este criterio se puede acceder a cualquier parámetro de red arbitrario utilizando esta representación.

Otra manera de acceder es utilizando la representación de índices, es decir, se puede acceder a los parámetros mediante índices dentro de corchetes. De esta forma, el índice inferior se numera desde el 1 y no desde 0, para lograr equivalencia de los índices de parámetros s. Por ejemplo, [1,1] representa \(s_{11}\).

s11 = netrf1[1,1]
print(s11 == netrf1.s11)
Salida:
True

La razón para mantener un parámetro dentro de un objeto de clase Network(), es que su manipulación posee todas las características de esta clase, por ejemplo, todas las opciones de representación gráfica están incluidas así como todas las transformaciones y demás características. Se puede obtener la lista completa ejecutando:

dir(netrf1)
Salida:
['COMPONENT_FUNC_DICT',
 ...
 'a',
 'a_arcl',
 'a_arcl_unwrap',
 'a_db',
 'a_db10',
 'a_deg',
 'a_deg_unwrap',
 'a_im',
 'a_mag',
 'a_rad',
 'a_rad_unwrap',
 'a_re',
 'a_time',
 'a_time_db',
 'a_time_impulse',
 'a_time_mag',
 'a_time_step',
 'a_vswr',
 'add_noise_polar',
 'add_noise_polar_flatband',
 'attribute',
 'comments',
 'copy',
 'copy_from',
 'copy_subset',
 'crop',
 'cropped',
 'deembed',
 'delay',
...
 'f',
 'f_noise',
 'flip',
 'flipped',
 'frequency',
...
 'h',
 'h_arcl',
 'h_arcl_unwrap',
 'h_db',
 'h_db10',
 'h_deg',
 'h_deg_unwrap',
 'h_im',
 'h_mag',
 'h_rad',
 'h_rad_unwrap',
 'h_re',
 'h_time',
 'h_time_db',
 'h_time_impulse',
 'h_time_mag',
 'h_time_step',
 'h_vswr',
 'impulse_response',
 'interpolate',
 'interpolate_self',
 'inv',
 'is_lossless',
 'is_passive',
 'is_reciprocal',
 'is_symmetric',
...
 'nports',
 'nudge',
 'number_of_ports',
 'params',
 'passivity',
...
 'plot_s_db',
 'plot_s_db10',
 'plot_s_db_time',
 'plot_s_deg',
 'plot_s_deg_unwrap',
 'plot_s_im',
 'plot_s_mag',
 'plot_s_polar',
 'plot_s_rad',
 'plot_s_rad_unwrap',
 'plot_s_re',
 'plot_s_smith',
 'plot_s_time',
 'plot_s_time_db',
 'plot_s_time_impulse',
 'plot_s_time_mag',
 'plot_s_time_step',
 'plot_s_vswr',
 'plot_t_arcl',
 'plot_t_arcl_unwrap',
 'plot_t_complex',
 'plot_t_db',
 'plot_t_db10',
 'plot_t_deg',
 'plot_t_deg_unwrap',
 'plot_t_im',
 'plot_t_mag',
...
 'renumbered',
 'resample',
 'rn',
 'rotate',
 's',
 's11',
 's12',
 's1_1',
 's1_2',
 's21',
 's22',
 's2_1',
 's2_2',
 's_active',
 's_arcl',
 's_arcl_unwrap',
 's_db',
 's_db10',
 's_def',
 's_deg',
 's_deg_unwrap',
 's_im',
 's_invert',
 's_mag',
 's_power',
 's_pseudo',
 's_rad',
 's_rad_unwrap',
 's_re',
 's_time',
 's_time_db',
 's_time_impulse',
 's_time_mag',
 's_time_step',
 's_traveling',
 's_vswr',
 'se2gmm',
 'set_noise_a',
 'stability',
 'stability_circle',
 'step_response',
 'subnetwork',
...
 'write_touchstone',
 'y',
 'y_active',
 'y_arcl',
 'y_arcl_unwrap',
 'y_db',
 'y_db10',
 'y_deg',
 'y_deg_unwrap',
 'y_im',
 'y_mag',
 'y_opt',
 'y_rad',
 'y_rad_unwrap',
 'y_re',
 'y_time',
 'y_time_db',
 'y_time_impulse',
 'y_time_mag',
 'y_time_step',
 'y_vswr',
 'z',
 'z0',
...
 'zipped_touchstone']

Nota: Por simplicidad la salida está truncada.

Manipulación de matrices NumPy

Si se prefiere manipular los datos de red como matrices NumPy, se puede utilizar la propiedad netrf1.s. Donde se obtiene un array de elementos de \(401 \times 2 \times 2\).

netrf1.s.shape
Salida
(401, 2, 2)
Notar que, ahora, los datos de la matriz están indexados a cero, como se espera en Python, se deberá sumar 1 a cada índice para encontrar la correspondencia con los parámetros s
s11 = netrf1.s[:,0,0] # s11
s21 = netrf1.s[:,1,0] # s21

NOTA: netrf1.s retorna arrays NumPy pero netrf1.s11 retorna un objeto Network().

netrf1.s11.s.shape
Salida:
(401, 1, 1)

Para llevar esas matrices de \(1 \times 1\) a números, se puede utilizar el método .squeeze().

s11=netrf1.s11.s.squeeze()
s11.shape
Salida
(401,)

Conversión en otros parámetros

Con skrf, es trivial convertir a otros parámetros de red (Y, Z, H, T y ABCD) accediendo unicamente a la propiedad adecuada del objeto Network.

Y = netrf1.y # Parámetros Admitancia
Z = netrf1.z # Parámetros Impedancia
H = netrf1.h # Parámetros Híbridos
T = netrf1.t # Parámetros Transferencia
A = netrf1.a # Parámetros ABCD
Y.shape
Salida:
(401, 2, 2)
El resultado, como se observa es un array NumPy que contiene las matrices de la red en todos los puntos de frecuencia.

Para convertir entre parámetros, skrf posee las siguientes funciones y2s, y2t, y2z, z2[a,h,s,t,y], t2[s,y,z], a2[s,z], f2[s,z]. Por ejemplo para pasar de parámetros Z a parámetros H se podría realizar de la siguiente forma:

Hc = rf.z2h(Z)

Manejo de formatos de datos

Los datos de Network() se almacenan como números complejos en arrays NumPy y se pueden convertir fácilmente en representaciones de módulo/fase, en real/imaginaria, en decibeles, ROE (VSWR, relación de onda estacionaria de tensión), entre otros, utilizando skrf sin tener que manipular los datos utilizando métodos NumPy.

Algunos ejemplos

  • s_db: El componente dB (\(20log_{10}\) por definirse en tensiones) de la matriz S.
  • s_db10: El componente dB10 (\(10log_{10}\)) de la matriz S.
  • s_deg: El componente angular en grados de la matriz S. Representación discontinua.
  • s_deg_unwrap: El componente angular grados de la matriz S. Representación continua.
  • s_im: El componente imaginario de la matriz S.
  • s_mag: El componente de módulo de la matriz S.
  • s_rad: El componente angular en radianes de la matriz S. Representación discontinua.
  • s_rad_unwrap: El componente angular en radianes de la matriz S. Representación continua.
  • s_re: El componente real de la matriz S.
  • s_vswr: La relación de onda estacionaria de tensión.

Determinación de la ROE en un puerto

s11=netrf1.s11
s11_vswr = s11.s_vswr.squeeze()

Extracción de frecuencias puntuales o rangos de frecuencia

Se puede crear un nuevo objeto Network() con un único punto de frecuencia (o un conjunto de ellas) pasándolo como un argumento string a un objeto Network() existente, por ejemplo:

netrf1_433 = netrf1['425MHz-445MHz']
print(netrf1_433)
Salida:
2-Port Network: 'bp401',  424977500.0-444987500.0 Hz, 117 pts, z0=[50.+0.j 50.+0.j]

Las unidades de frecuencia no distinguen entre mayúsculas y minúsculas. El nuevo objeto Network() se ha reducido a \(117\) puntos de frecuencia entre \(425\:MHz\) y \(445\:MHz\). Por otro lado, admite declaraciones de rangos en otros múltipos y divisores de la unidad frecuencia, puede especificarse de igual manera 425000000Hz-445000000Hz, por ejemplo.

Además, con este criterio, se puede extraer de forma muy sencilla un único valor correspondiente a un punto en una frecuencia determinada.

s11_433 = netrf1['433mhz'].s11.s_db.item()
print(s11_433)
Salida:
-22.844283912124858

No especificar la unidad, contemplará la unidad definida en unit.

netrf1.frequency.unit
Salida:
'Hz'

Interpolación y extrapolación

Si se especifica un punto o rango de frecuencia que no forma parte del conjunto de datos de frecuencia original, pero que aún se encuentra dentro del rango de frecuencias disponibles, se interpolan las matrices para devolver el valor en la frecuencia especificada. Por ejemplo, entre dos elementos de la siguiente porción del archivo .s2p

. . .
      433775000    1.074017361e-01   -1.250087563e-02    3.138242662e-01    7.182558179e-01    0.000000000e+00    0.000000000e+00    0.000000000e+00    0.000000000e+00
      433947500    1.170873493e-01   -1.804022491e-02    4.049844444e-01    6.635542512e-01    0.000000000e+00    0.000000000e+00    0.000000000e+00    0.000000000e+00
. . .
que se posicionan a \(433,775000\:MHz\) y \(433,947500\:MHz\) la parte real de \(s_{21}\) va de \(3,138242662\times 10^{-1}\) a \(4,049844444\times 10^{-1}\). Sea, por ejemplo, se desee conocer la parte real de \(s_{11}\:433,8\:MHz\) y \(433,9\:MHz\) que no están en el conjunto de datos original.

netrf1['433.8 MHz'].s
Salida:
array([[[0.10740174-0.01250088j, 0.        +0.j        ],
        [0.31382427+0.71825582j, 0.        +0.j        ]]])

netrf1['433900000'].s
Salida:
array([[[0.11708735-0.01804022j, 0.        +0.j        ],
        [0.40498444+0.66355425j, 0.        +0.j        ]]])
En este caso corresponde a los valores respectivos más cercanos. Con lo que la interpolación se realiza por medio de asociar el elemento más cercano. Esto dispara la siguiente pregunta, ¿Qué sucede en la mitad del rango a interpolar? Para dar respuesta, se determina el punto medio entre los dos puntos de frecuencia definidos (433.775-433.9475)/2+433.9475 que arroja 433.86125 luego netrf1['433.86125 MHz'].s arroja la siguiente salida:
array([[[0.10740174-0.01250088j, 0.        +0.j        ],
        [0.31382427+0.71825582j, 0.        +0.j        ]]])
Ha reemplazado por el valor más cercano a la frecuencia inferior del rango. Ahora se evalúa que pasa para un incremento de \(10\:Hz\) netrf1['433.86126 MHz'].s arrojando la siguiente salida:
array([[[0.11708735-0.01804022j, 0.        +0.j        ],
        [0.40498444+0.66355425j, 0.        +0.j        ]]])
Que ha reemplazado por el valor más cercano a la frecuencia superior del rango.

Como conclusión se puede decir que el proceso de interpolación de una determinada frecuencia se realiza fijando el componente \(s\) al valor más cercano a la frecuencia discreta definida.

Si el punto o rango de frecuencia supera el límite inferior o superior, el objeto de red se fija en el punto de frecuencia inferior o superior respectivamente. Por ejemplo, netrf1['400MHz'].s y netrf1['500MHz'].s arrojan respectivamente:

array([[[ 4.95522678e-01-9.14786518e-01j,
          0.00000000e+00+0.00000000e+00j],
        [-1.32181478e-04-3.94408154e-04j,
          0.00000000e+00+0.00000000e+00j]]])
y
array([[[ 7.27955878e-01-6.17048502e-01j,
          0.00000000e+00+0.00000000e+00j],
        [-3.20424675e-04+2.22774106e-04j,
          0.00000000e+00+0.00000000e+00j]]])
Que, como puede observase, son los valores correspondientes a los límites del archivo Touchstone.
. . .
      401000000    4.955226779e-01   -9.147865176e-01   -1.321814780e-04   -3.944081545e-04    0.000000000e+00    0.000000000e+00    0.000000000e+00    0.000000000e+00
. . .
      470000000    7.279558778e-01   -6.170485020e-01   -3.204246750e-04    2.227741061e-04    0.000000000e+00    0.000000000e+00    0.000000000e+00    0.000000000e+00

Uso de matrices NumPy

Siendo que netrf1.s, devuelve una matriz de la forma (401, 2, 2), donde el primer elemento corresponde a la cantidad de frecuencias discretas, y el resto de los elementos definen la matriz cuadrada en base a la cantidad de puertos de la red. Una utilidad para encontrar un índice de una determinada frecuencia es comparar la matriz de frecuencias con el valor requerido utilizando por ejemplo netrf1.s == 470000000, arrojará como resultado una matriz de valores booleanos que indican si la condición se ha cumplido o no. Esta matriz booleana se puede utilizar en lugar del índice de frecuencia para encontrar la matriz de parámetros s a esa frecuencia de la siguiente forma netrf1.s[netrf1.f == 470000000,:,:] que arroja: array([[[ 7.27955878e-01-6.17048502e-01j, 0.00000000e+00+0.00000000e+00j], [-3.20424675e-04+2.22774106e-04j, 0.00000000e+00+0.00000000e+00j]]]) Esto no será util si no se conoce especificamente el valor exacto del punto de frecuencia existente. Por ello, acceder por medio de un rango de frecuencias puede resultar de mayor utilidad, se puede utilizar logical_and() (previamente se debe importar NumPy con import numpy as np) para devolver una matriz booleana que retorna True para cada frecuencia que cumpla con las condiciones especificadas y, False si no las cumple. Por ejemplo para extraer el rango de la banda de paso del filtro por ejemplo entre \(428\:MHz\) y \(438\:MHz\) se puede ejecutar netrf1.s[np.logical_and(netrf1.f >= 428e6, netrf1.f <= 428e6),:,:] Esto le retorna los mismos datos de red en forma de matrices NumPy que cuando usa netrf1['428-438MHZ'].s.

Ejemplos

  • Ejemplo para obtener el máximo valor de módulo del parámetro \(s_{21}\) s21_max=np.argmax(np.abs(netrf1.s[:,1,0]))
  • Ejemplo para obtener el índice del máximo valor de módulo del parámetro \(s_{21}\) idx_s21_max=np.argmax(np.abs(netrf1.s[:,1,0]))
  • Ejemplo para obtener los índices que están dentro del ancho de banda del filtro idx_s21_bw=np.where(np.abs(netrf1.s[:,1,0]) > 0.707*s21_max)
  • Ejemplo para obtener las matrices en la banda de paso netrf1.s[idx_s21_bw,:,:]
  • Ejemplo para obtener la frecuencia de corte superior fcs=netrf1.f[idx_s21_bw[-1]]
  • Ejemplo para obtener la frecuencia de corte inferior fci=netrf1.f[idx_s21_bw[0]]

Visualización

Las funciones de trazado integradas utilizan matplotlib en segundo plano. La clase Network() tiene métodos que son convenientes para usar para trazar en proyecciones rectangulares, complejas, polares y diagramas Smith.

Diagrama de módulo en dB

Es un diagrama cartesiano en donde se grafican los módulos de los parámetros s en escala de \(dB\) como ordenadas al origen y la frecuencia en escala lineal el el eje de abscisas. Cuando no se proporcionan argumentos a la función de trazado, todos los parámetros de red disponibles se trazan de forma predeterminada.

netrf1.s21.plot_s_db(label='Banda completa de respuesta')
netrf1.s21['430-438MHz'].plot_s_db(label='Banda de interés',lw=4)
image

Diagrama de Smith

Muestra la variación de la impedancia o admitancia compleja de una línea de transmisión a lo largo de su longitud. Se usa frecuentemente para simplificar la adaptación de la impedancia de una línea de transmisión con su carga.

Para graficar, simplemente se ejecuta:

`netrf1.plot_s_smith()`

Se obtiene:

image

Cuando no se proporcionan argumentos a la función de trazado, todos los parámetros de red disponibles se trazan de forma predeterminada.

En una red de dos puertos como la que se utiliza aquí, se representan gráficamente los cuatro parámetros de red \(s_{11}\), \(s_{12}\), \(s_{21}\) y \(s_{22}\). Además, el nombre del objeto de Network() se utiliza en la etiqueta del gráfico.

Se puede representar gráficamente \(s_{11}\) en el diagrama de Smith si se configuran los índices a m=0 y n=0. Además, r=1 establece el radio del diagrama de Smith en \(1\), que es el valor predeterminado. El diagrama de Smith permite crear diagramas de impedancia o admitancia según si chart_type está configurado en z o y, respectivamente. También, se puede desactivar la leyenda con show_legend=False. Otra visualización interesante en el diagrama de Smith es la inclusión de círculos equi-ROE, que se habilitan mediante la palabra draw_vswr=True, y encerrarián zonas que contienen Relaciones de Onda Estacionarias inferiores a los valores consignados en los círculos. La palabra draw_labels=True muestra las etiquetas de los ejes del diagrama de Smith a lo largo del eje real y del eje de impedancia/admitancia. También coloca etiquetas en los círculos ROE. Por ejemplo la obtención del Diagrama de Smith del componente de red \(s_{11}\) en el entorno de la banda de paso, puede obtenerse con el siguiente comando netrf1['431-435MHz'].plot_s_smith(m=0,n=0,r=1,chart_type='z',show_legend=True,draw_labels=True,draw_vswr=True):

image

Donde, puede observase, que en la banda de paso, el cuadripolo presenta impedancia de naturaleza resistiva en un entorno de \(50\:\Omega\), y la ROE es inferior a \(1,5\).

Interconexión de redes

Interconexión de redes en cascada

Las conexiones en cascada y los procedimientos de "desincrustación" (de-embedding) de redes de dos puertos se pueden realizar fácilmente mediante operadores. Una conexión en cascada se puede llamar a través del operador de potencia **. Para calcular una nueva red que sea la conexión en cascada de las dos redes individuales.

Por ejemplo, se posee dos redes definidas los archivos Touchstone hp15m.s2p y lp18m.s2p y se las desea conectar en cascada de la siguiente forma:

    +------ netrf4 ------+

    +-------+    +-------+ 
1---| hp15m |----| lp18m |---2
    +-------+    +-------+ 
hp15m = rf.Network("hp15m.s2p")
lp18m = rf.Network("lp18m.s2p")
netrf4= hp15m ** lp18m

hp15m.plot_s_db(m=1,n=0)
lp18m.plot_s_db(m=1,n=0)
netrf4.plot_s_db(m=1,n=0)

image

NOTA: La curva verde es la respuesta de netrf4 y no de hp15m, evidentemente skrf toma como nombre de la red, el nombre de la primera red de la cascada.

De-embedding

El procedimiento de-embedding, muy utilizado en los procesos de medición para caracterizar la red de interés, se puede lograr conectando en cascada la inversa de una red. La inversa de una red a través de la propiedad Network.inv.

Por ejemplo para obtener la red hp15m a partir de las redes netrf4 y lp18m el procedimiento es el siguiente:

    +-------- hp15m ---------+

    +--------+    +----------+ 
1---| netrf4 |----| lp18m^-1 |---2
    +--------+    +----------+ 

hp15m_ = netrf4 ** lp18m.inv
hp15m_ == hp15m
True
Donde se observa la igualdad en la red original hp15m y su obtención de-embedding. De igual modo, si se desea obtener la red lp18m a partir de las redes netrf4 y hp15m el procedimiento es el siguiente:
    +-------- lp18m ---------+

    +----------+    +--------+ 
1---| hp15m^-1 |----| netrf4 |---2
    +----------+    +--------+ 
lp18m_ = hp15m.inv ** netrf4
lp18m_ == lp18m

Fuentes