
Figure 1:Quasy-Vertical Profile - QVP. Fuente: Ryzhkov et al., 2016
Quasy-Vertical Profiles¶
🧭 Introducción¶
En este cuadernillo exploraremos el uso de Perfiles Cuasi-Verticales (QVPs) como una técnica eficiente para analizar la estructura vertical de la atmósfera utilizando datos de radar meteorológico polarimétrico. Mostraremos cómo generar y visualizar QVPs a partir de datos almacenados en formato Zarr mediante el flujo de trabajo ARCO, y compararemos su eficiencia frente al enfoque tradicional basado en archivos binarios.
📚 ¿Qué vas a aprender?¶
✔ Qué es un Perfil Cuasi-Vertical (QVP) y su utilidad en radar meteorológico polarimétrico
✔ Cómo acceder a datos de radar preprocesados en formato Zarr con ARCO
✔ Cómo calcular QVPs para distintas variables radar
✔ Cómo comparar el flujo de trabajo ARCO con un enfoque tradicional basado en archivos binarios
✔ Visualización de perfiles verticales de reflectividad, reflectividad diferencial, correlación cruzada y fase diferencial
✅ Requisitos Previos¶
Concepto | Importancia | Enlace sugerido |
---|---|---|
Fundamentos de Python | Necesario | Fundamentos del lenguaje y estructuras básicas |
Xarray | Necesario | Manipulación de arreglos y datos multidimensionales |
Radar meteorológico | Necesario | Conceptos básicos de adquisición y estructura de datos de radar |
Zarr y flujos de trabajo FAIR | Útil | Concepto de almacenamiento en la nube y acceso eficiente a grandes volúmenes de datos |
Artículo sobre QVPs | Útil | Fundamento teórico sobre Perfiles Cuasi-Verticales (Ryzhkov et al., 2016) |
1. 📡 ¿Qué es un Perfil Cuasi-Vertical (QVP)?¶
Los Perfiles Cuasi-Verticales (Quasi-Vertical Profiles, QVPs) constituyen una técnica utilizada en el análisis de datos de radar meteorológico polarimétrico. Esta técnica consiste en promediar la información de una elevación del radar (típicamente 20°) a lo largo de todos los azimuts generar un perfil vertical simplificado, que permite representar la estructura vertical de la atmósfera en las cercanías del radar como se observa en la siguiente figura

Figure 2:Esquema conceptual de un perfil cuasi-vertical (QVP), donde se promedia azimutalmente una elevación específica del radar para construir un perfil vertical.
Fuente: Ryzhkov et al., 2016
Los QVPs resultan útiles para:
- Estudiar la evolución microfísica de tormentas (como el crecimiento de cristales de hielo o procesos de fusión de hidrometeoros),
- Monitorear la fase de precipitación,
- Evaluar transiciones entre fases líquidas y sólidas en sistemas de precipitación.
from dask.distributed import Client, LocalCluster
# Create a local cluster with correct arguments
cluster = LocalCluster(
n_workers=4, # Number of worker processes
memory_limit='2GB' # Per worker memory limit
)
client = Client(cluster)
client
2. 🗂️ Acceso a datos ARCO en formato Zarr¶
En esta sección accederemos a un conjunto de datos de radar meteorológico preprocesado, almacenado en formato Zarr v3 en un bucket público mantenido por el proyecto Pythia.
Los datos corresponden al radar KVNX (Vance AFB, Oklahoma) para el evento del 20 de mayo de 2011, y están disponibles gracias a la iniciativa ARCO-FAIR. Estos datos se encuentran almacenados de manera eficiente en la nube (Jetstream 2), lo que permite el acceso distribuido y escalable sin necesidad de descarga previa.
import xarray as xr
import xradar
import fsspec
# Ruta al archivo Zarr en la nube
url = "https://js2.jetstream-cloud.org:8001/"
path = "s3://pythia/radar/AtmosCol2025/KVNX.zarr"
# Abrimos nuesto dataset
dtree = xr.open_datatree(
path,
engine="zarr",
chunks={},
backend_kwargs={
"consolidated": False,
"storage_options": {
"anon": True,
"client_kwargs": {
"endpoint_url": url
}
}
}
)
display(dtree)
print(f"Tamaño del conjunto de datos es: {dtree.nbytes / 1024**3: .2f} GB")
Tamaño del conjunto de datos es: 6.99 GB
2.1 🗂️ Exploración del DataTree¶
Una vez cargado el dataset en formato DataTree
, podemos inspeccionar su estructura jerárquica. Esta estructura refleja la organización típica de un archivo de radar con múltiples elevaciones (sweeps), metadatos globales y parámetros del radar.
Podemos listar los nodos hijos principales del árbol de datos:
list(dtree.children)
['VCP-12']
Luego podemos explorar los niveles inferiores, por ejemplo:
# Exploramos los hijos del volumen VCP-12
list(dtree["VCP-12"].children)
['georeferencing_correction',
'sweep_14',
'sweep_15',
'sweep_13',
'sweep_16',
'radar_parameters',
'sweep_2',
'sweep_0',
'sweep_3',
'sweep_10',
'sweep_5',
'sweep_1',
'sweep_12',
'sweep_11',
'sweep_4',
'sweep_8',
'sweep_9',
'sweep_6',
'sweep_7']
Cada sweep_N
representa un barrido del radar a un ángulo de elevación distinto. Además, encontramos nodos como radar_parameters
y georeferencing_correction
.
También es posible inspeccionar directamente una variable meteorológica, como la reflectividad horizontal (DBZH
) en un sweep específico:
2.2 📁 Acceso a nodos con sintaxis de ruta (file-path syntax
)¶
Una ventaja del uso de xarray-datatree
es que permite acceder a cualquier nodo o variable utilizando rutas tipo archivo, lo que simplifica el manejo jerárquico de los datos.
Seleccionaremos la variable DBZH
desde el nodo correspondiente utilizando la sintaxis tipo archivo:
# Forma recomendada
dbzh = dtree["/VCP-12/sweep_16/DBZH"]
display(dbzh)
3. ⚙️ Cálculo de Perfiles Cuasi-Verticales (QVPs)¶
Una vez explorada la estructura de datos, procedemos a calcular los Perfiles Cuasi-Verticales (QVPs). Este tipo de perfil se genera promediando una variable de radar a lo largo de todos los azimuts en un barrido de elevación fijo, lo que proporciona una vista simplificada pero útil de la estructura vertical de la atmósfera.
3.1. 📥 Selección del sweep y del período de interés¶
Trabajaremos con el sweep_16
, correspondiente a un ángulo de elevación bajo, adecuado para QVPs. Además, seleccionamos un intervalo de tiempo entre 10:00 y 12:00 UTC del 20 de mayo de 2011, con el objetivo de reproducir los resultados presentados en Ryzhkov et al. (2016), quienes analizaron un sistema convectivo de mesoescala (MCS) observado con el radar KVNX en esa misma fecha.
ds_qvp = dtree["/VCP-12/sweep_16"].ds.sel(
vcp_time=slice("2011-05-20 10:00", "2011-05-20 12:00")
)
3.2 🧮 Función para calcular un QVP¶
Una vez seleccionados los datos del barrido adecuado, definimos una función que calcule el Perfil Cuasi-Vertical (QVP) a partir de cualquier variable radar que siga el esquema tridimensional típico (vcp_time
, azimuth
, range
).
La función realiza el promedio azimutal y, si la variable está en escala logarítmica (como dBZ
), realiza la conversión a escala lineal antes del promedio y luego la convierte nuevamente a logarítmica. También transforma la dimensión range
a height
(en km) usando el ángulo de elevación medio.
import numpy as np
def compute_qvp(ds: xr.Dataset, var="DBZH") -> xr.DataArray:
"""
Calcula un QVP (Perfil Cuasi-Vertical) para una variable específica del radar.
Parámetros:
----------
ds : xr.Dataset
Dataset que contiene la variable radar.
var : str
Nombre de la variable a procesar (ej. 'DBZH', 'ZDR', 'RHOHV', 'PHIDP').
Retorna:
--------
xr.DataArray
QVP con coordenadas [vcp_time, height] y atributos preservados.
"""
units = ds[var].attrs.get("units", "")
# Conversión de logarítmico a lineal si es necesario
if units.startswith("dB"):
qvp = 10 ** (ds[var] / 10)
qvp = qvp.mean("azimuth", skipna=True)
qvp = 10 * np.log10(qvp)
else:
qvp = ds[var].mean("azimuth", skipna=True)
# Conversión de distancia a altura estimada (en km)
elev_rad = ds.sweep_fixed_angle.mean(skipna=True).values * np.pi / 180.0
qvp = qvp.assign_coords({
"range": (qvp.range.values * np.sin(elev_rad)) / 1000 # convierte a km
})
qvp = qvp.rename(f"qvp_{var}")
qvp = qvp.rename({"range": "height"})
return qvp
3.3 🔄 Cálculo de QVPs para múltiples variables¶
Una vez definida la función compute_qvp()
, procedemos a aplicarla a las principales variables polarimétricas del radar: DBZH
, ZDR
, RHOHV
y PHIDP
.
Estas variables ofrecen información complementaria sobre la estructura microfísica de la atmósfera:
DBZH
: Reflectividad horizontal, relacionada con la intensidad de la precipitación.ZDR
: Reflectividad diferencial, útil para distinguir tipos de hidrometeoros.RHOHV
: Coeficiente de correlación cruzada, indica homogeneidad de las partículas.PHIDP
: Fase diferencial, relacionada con el contenido de agua líquida.
%%time
qvp_ref = compute_qvp(ds_qvp, var="DBZH").compute()
qvp_zdr = compute_qvp(ds_qvp, var="ZDR").compute()
qvp_rhohv = compute_qvp(ds_qvp, var="RHOHV").compute()
qvp_phidp = compute_qvp(ds_qvp, var="PHIDP").compute()
CPU times: user 641 ms, sys: 68.8 ms, total: 710 ms
Wall time: 8.52 s
📈 4. Visualización de Perfiles Cuasi-Verticales (QVPs)¶
4.1. 🖼️ Función para graficar QVPs en panel 2×2¶
Definimos una función para visualizar los Perfiles Cuasi-Verticales (QVPs) de las variables más comunes en radar polarimétrico. Esto facilita la comparación entre distintos métodos (Zarr vs binario) o eventos meteorológicos.
import matplotlib.pyplot as plt
import cmweather
def plot_qvp_panels(qvp_ref, qvp_zdr, qvp_rhohv, qvp_phidp):
"""
Visualiza perfiles cuasi-verticales (QVPs) en un panel 2x2 con formato científico completo.
Parámetros:
-----------
qvp_ref : xr.DataArray
Reflectividad (DBZH)
qvp_zdr : xr.DataArray
Reflectividad diferencial (ZDR)
qvp_rhohv : xr.DataArray
Coeficiente de correlación cruzada (RHOHV)
qvp_phidp : xr.DataArray
Fase diferencial (PHIDP)
title : str
Título de la figura
"""
fig, axs = plt.subplots(2, 2, figsize=(12, 6), sharex=True, sharey=True)
# Panel 1: Reflectividad (DBZH)
cf = qvp_ref.plot.contourf(
x="vcp_time", y="height", cmap="ChaseSpectral",
levels=np.arange(-10, 55, 1), ax=axs[0][0], add_colorbar=False
)
contour = qvp_ref.plot.contour(
x="vcp_time", y="height", levels=np.arange(-10, 50, 10),
colors="k", ax=axs[0][0]
)
axs[0][0].clabel(contour, fmt="%d", inline=True, fontsize=8)
axs[0][0].set_title(r"$Z$")
axs[0][0].set_ylabel(r"$Height \ [km]$")
axs[0][0].set_ylim(0, 7)
plt.colorbar(cf, ax=axs[0][0], label=r"$Reflectivity \ [dBZ]$")
# Panel 2: Reflectividad diferencial (ZDR)
cf1 = qvp_zdr.plot.contourf(
x="vcp_time", y="height", cmap="ChaseSpectral",
levels=np.linspace(-1, 5, 11), ax=axs[0][1], add_colorbar=False
)
contour = qvp_ref.plot.contour(
x="vcp_time", y="height", levels=np.arange(-10, 50, 10),
colors="k", ax=axs[0][1]
)
axs[0][1].clabel(contour, fmt="%d", inline=True, fontsize=8)
axs[0][1].set_title(r"$Z_{DR}$")
axs[0][1].set_ylabel("")
axs[0][1].set_xlabel("")
plt.colorbar(cf1, ax=axs[0][1], label=r"$Diff. \ Reflectivity \ [dB]$")
# Panel 3: RHOHV
cf2 = qvp_rhohv.plot.contourf(
x="vcp_time", y="height", cmap="Carbone11",
levels=np.arange(0.8, 1.01, 0.01), ax=axs[1][0], add_colorbar=False
)
contour = qvp_ref.plot.contour(
x="vcp_time", y="height", levels=np.arange(-10, 50, 10),
colors="k", ax=axs[1][0]
)
axs[1][0].clabel(contour, fmt="%d", inline=True, fontsize=8)
axs[1][0].set_title(r"$\rho _{HV}$")
axs[1][0].set_ylabel(r"$Height \ [km]$")
axs[1][0].set_xlabel(r"$Time \ [UTC]$")
plt.colorbar(cf2, ax=axs[1][0], label=r"$Cross-Correlation \ Coef.$")
# Panel 4: PHIDP
cf3 = qvp_phidp.plot.contourf(
x="vcp_time", y="height", cmap="PD17",
levels=np.arange(0, 110, 5), ax=axs[1][1], add_colorbar=False
)
contour = qvp_ref.plot.contour(
x="vcp_time", y="height", levels=np.arange(-10, 50, 10),
colors="k", ax=axs[1][1]
)
axs[1][1].clabel(contour, fmt="%d", inline=True, fontsize=8)
axs[1][1].set_title(r"$\theta _{DP}$")
axs[1][1].set_xlabel(r"$Time \ [UTC]$")
axs[1][1].set_ylabel("")
plt.colorbar(cf3, ax=axs[1][1], label=r"$Differential \ Phase \ [deg]$")
fig.tight_layout(rect=[0, 0.03, 1, 0.95])
plt.show()
4.2 📊 Visualización de QVPs usando función reutilizable¶
Ahora utilizamos la función plot_qvp_panels()
para graficar los Perfiles Cuasi-Verticales (QVPs) de las variables DBZH
, ZDR
, RHOHV
y PHIDP
, obtenidos a partir de los datos del radar KVNX para el evento del 20 de mayo de 2011.
%%time
plot_qvp_panels(
qvp_ref=qvp_ref,
qvp_zdr=qvp_zdr,
qvp_rhohv=qvp_rhohv,
qvp_phidp=qvp_phidp,
)

CPU times: user 1.18 s, sys: 56.5 ms, total: 1.24 s
Wall time: 1.21 s
El flujo completo —desde el acceso a datos remotos hasta el cálculo y visualización de los QVPs— se realiza de forma muy eficiente gracias al formato Zarr y la compatibilidad con lectura directa vía fsspec
.
- Cálculo de QVPs: ~11.9 segundos
- Visualización completa (panel 2×2): ~1.4 segundos
5. ⏱️ Comparación con flujo de trabajo tradicional¶
En esta sección contrastamos el enfoque moderno y reproducible basado en datos ARCO-Zarr con el flujo tradicional que utiliza archivos binarios NEXRAD. Este último requiere:
- Acceso manual a archivos
.gz
desde AWS S3 - Descompresión local
- Conversión a
xarray.Dataset
conxradar
- Concatenación secuencial o paralela
5.1 📥 Descarga y apertura de archivos NEXRAD¶
Usamos fsspec
para acceder directamente al bucket público de NEXRAD en AWS, seguido por descompresión con gzip
. Creamos la siguente función para descargar los archivos de manera local:
import fsspec
import tempfile
import os
import xradar
import gzip
def nexrad_download(file):
local_file = fsspec.open_local(
f"simplecache::s3://{file}",
s3={"anon": True},
filecache={"cache_storage": "."},
)
with gzip.open(local_file, "rb") as gz:
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
temp_file.write(gz.read())
temp_file_path = temp_file.name
try:
data_tree = xradar.io.open_nexradlevel2_datatree(temp_file_path)
finally:
os.remove(local_file)
os.remove(temp_file_path)
return data_tree
5.2 🧪 Creación del dataset QVP¶
Seleccionamos 35 archivos del radar KVNX
correspondientes al evento del 20 de mayo de 2011 entre 10:00 y 12:00 UTC. Cada archivo contiene un volumen escaneado; extraemos el sweep_16
.
import pandas as pd
fs = fsspec.filesystem("s3", anon=True)
radar = "KVNX"
bucket = "s3://noaa-nexrad-level2/"
query = f"2011/05/20/{radar}/{radar}"
files = [f"s3://{f}" for f in sorted(fs.glob(f"{bucket}{query}*"))][135:170]
len(files)
35
Después de identificar los archivos, procedemos a descargarlos, descomprimirlos y extraer el barrido sweep_16
de cada volumen. Luego concatenamos todos los datasets a lo largo del eje vcp_time
.
%%time
datasets = []
times = []
for f in files:
dtree = nexrad_download(f)
times.append(pd.to_datetime(dtree.time_coverage_start.item()))
datasets.append(dtree["sweep_16"].to_dataset())
ds_trad_qvp = xr.concat(datasets, dim="vcp_time")
ds_trad_qvp = ds_trad_qvp.assign_coords(
vcp_time=[t.tz_convert(None) if t.tzinfo else t for t in times]
)
CPU times: user 51.4 s, sys: 7.31 s, total: 58.7 s
Wall time: 2min 11s
5.3 🔄 Cálculo de QVPs¶
Una vez creado el dataset concatenado a partir de los archivos binarios, utilizamos la función compute_qvp()
para generar los Perfiles Cuasi-Verticales (QVPs) de las principales variables polarimétricas del radar.
%%time
qvp_trad_ref = compute_qvp(ds_trad_qvp, var="DBZH")
qvp_trad_zdr = compute_qvp(ds_trad_qvp, var="ZDR")
qvp_trad_rhohv = compute_qvp(ds_trad_qvp, var="RHOHV")
qvp_trad_phidp = compute_qvp(ds_trad_qvp, var="PHIDP")
CPU times: user 1.12 s, sys: 192 ms, total: 1.32 s
Wall time: 1.27 s
5.4 📊 Visualización de QVPs tradicionales¶
Para visualizar los QVPs generados desde archivos binarios NEXRAD, reutilizamos la misma función utilizada anteriormente:
plot_qvp_panels(
qvp_ref=qvp_trad_ref,
qvp_zdr=qvp_trad_zdr,
qvp_rhohv=qvp_trad_rhohv,
qvp_phidp=qvp_trad_phidp,
)

El flujo tradicional para procesar y visualizar QVPs con archivos binarios NEXRAD implica una carga computacional y operativa significativamente mayor:
- Descarga y lectura secuencial de archivos: ~2 minutos 20 segundos
- Cálculo de QVPs: ~1.6 segundos
- Visualización: ~1.4 segundos
📝 Conclusiones¶
En este cuaderno comparamos dos enfoques para el cálculo de Perfiles Cuasi-Verticales (QVPs) a partir de datos de radar polarimétrico:
- 🌀 Flujo tradicional usando archivos binarios NEXRAD (
.gz
) - 🧊 Flujo moderno FAIR usando datos preprocesados en formato Zarr (ARCO)
La siguiente tabla resume los tiempos aproximados y la aceleración lograda en cada etapa:
Etapa | ARCO-Zarr | Binario NEXRAD | Aceleración (×) |
---|---|---|---|
Acceso / lectura de datos | ~0 s (lazy loading) | ~140 s | ~140× |
Cálculo de QVPs | ~11.9 s | ~1.6 s | 0.13× |
Visualización | ~1.4 s | ~1.4 s | = |
Total estimado | ~13.3 s | ~143.0 s | ~10.7× |
- Ryzhkov, A., Zhang, P., Reeves, H., Kumjian, M., Tschallener, T., Trömel, S., & Simmer, C. (2016). Quasi-Vertical Profiles—A New Way to Look at Polarimetric Radar Data. Journal of Atmospheric and Oceanic Technology, 33(3), 551–562. 10.1175/jtech-d-15-0020.1