2026-11-05 17:26:55
This commit is contained in:
69
README.md
69
README.md
@@ -1 +1,68 @@
|
|||||||
test publico
|
# ZarrViewer
|
||||||
|
|
||||||
|
Un explorador rápido y ligero para archivos `.zarr` desde la terminal. Permite visualizar la estructura, los tiempos y generar gráficos inteligentes (mapas o series de tiempo) con un solo comando.
|
||||||
|
|
||||||
|
## Instalación
|
||||||
|
|
||||||
|
1. Asegúrate de tener Python 3.6 o superior.
|
||||||
|
|
||||||
|
2. Instala las dependencias necesarias:
|
||||||
|
```bash
|
||||||
|
pip install xarray pandas zarr matplotlib
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Convierte el script en un comando nativo del sistema:
|
||||||
|
```bash
|
||||||
|
# Quítale la extensión .py
|
||||||
|
mv zarrviewer.py zarrviewer
|
||||||
|
|
||||||
|
# Dale permisos de ejecución
|
||||||
|
chmod +x zarrviewer
|
||||||
|
|
||||||
|
# Muévelo a un directorio en tu PATH (ej. ~/.local/bin o /usr/local/bin)
|
||||||
|
mv zarrviewer ~/.local/bin/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Modo de Uso
|
||||||
|
|
||||||
|
### 1. Exploración Básica (Listar variables y estructura)
|
||||||
|
Si pasas solo la ruta del archivo (o usas la bandera `-h`), el programa imprimirá la estructura completa del archivo (similar a `ncdump -h`) y listará todas las variables disponibles para graficar.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
zarrviewer ruta/al/archivo.zarr
|
||||||
|
# o
|
||||||
|
zarrviewer ruta/al/archivo.zarr -h
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Ver Tiempos Disponibles
|
||||||
|
Muestra el inicio y el final de la serie de tiempo disponible.
|
||||||
|
```bash
|
||||||
|
zarrviewer ruta/al/archivo.zarr
|
||||||
|
```
|
||||||
|
Opciones adicionales:
|
||||||
|
* `-f 10`: Muestra los primeros 10 tiempos (por defecto son 3).
|
||||||
|
* `-l 10`: Muestra los últimos 10 tiempos.
|
||||||
|
* `-a`: Imprime todos los tiempos disponibles en el archivo.
|
||||||
|
|
||||||
|
### 3. Generar un Mapa Geográfico (2D)
|
||||||
|
Si la variable tiene 2 dimensiones o más (ej. Lat/Lon, o X/Y en WRF), generará automáticamente un mapa georreferenciado del **último frame temporal disponible** con la escala corregida (1:1).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
zarrviewer archivo.zarr -v T2
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Generar una Serie de Tiempo (1D o Estaciones)
|
||||||
|
Si la variable está asociada a una dimensión unidimensional (como estaciones meteorológicas), debes especificar el valor/estación deseada separada por dos puntos `:` o un punto `.`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ejemplo: Graficar la variable 'wind_speed_5m_prom' en la estación 'LSC'
|
||||||
|
zarrviewer archivo.zarr -v wind_speed_5m_prom:LSC
|
||||||
|
```
|
||||||
|
|
||||||
|
**Sugerencia Interactiva:** Si olvidas especificar la estación, el programa te listará todos los valores posibles y te dará ejemplos listos para copiar y pegar:
|
||||||
|
```bash
|
||||||
|
zarrviewer archivo.zarr -v wind_speed_5m_prom
|
||||||
|
```
|
||||||
|
|
||||||
|
Opciones de graficado:
|
||||||
|
* `-n 100`: Número de últimos registros a incluir en la serie de tiempo (por defecto extrae 50 puntos).
|
||||||
|
|||||||
249
zarrviewer
Executable file
249
zarrviewer
Executable file
@@ -0,0 +1,249 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
zarrviewer: Visualiza archivos Zarr, muestra su estructura y grafica mapas o series de tiempo.
|
||||||
|
Uso básico: zarrviewer ruta/al/archivo.zarr/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import xarray as xr
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Explorador y graficador rápido para archivos Zarr.",
|
||||||
|
add_help=False
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument("--help", action="help", default=argparse.SUPPRESS,
|
||||||
|
help="Muestra este mensaje de ayuda y termina")
|
||||||
|
parser.add_argument("zarr_path", help="Ruta al directorio del archivo Zarr")
|
||||||
|
parser.add_argument("--header", "-h", action="store_true",
|
||||||
|
help="Muestra la estructura/metadata del archivo Zarr (ncdump -h)")
|
||||||
|
parser.add_argument("--first", "-f", type=int, default=3,
|
||||||
|
help="Número de tiempos iniciales a mostrar (default: 3)")
|
||||||
|
parser.add_argument("--last", "-l", type=int, default=3,
|
||||||
|
help="Número de tiempos finales a mostrar (default: 3)")
|
||||||
|
parser.add_argument("--all", "-a", action="store_true",
|
||||||
|
help="Mostrar todos los tiempos")
|
||||||
|
parser.add_argument("--coord", "-c", type=str, default=None,
|
||||||
|
help="Nombre de la coord. temporal (auto-detectada por defecto)")
|
||||||
|
parser.add_argument("--variable", "-v", type=str, default=None,
|
||||||
|
help="Variable a graficar. Sintaxis: var, var:dim, var.dim o 'var(dim)'")
|
||||||
|
parser.add_argument("-n", "--npoints", type=int, default=50,
|
||||||
|
help="Número de registros a graficar en series de tiempo (default: 50)")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
ds = xr.open_zarr(args.zarr_path)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error al abrir el Zarr: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# 1. Si no hay más argumentos que el archivo (o si se pidió el header), mostramos información útil.
|
||||||
|
if len(sys.argv) == 2 or args.header:
|
||||||
|
print("="*60)
|
||||||
|
print(f" ESTRUCTURA DEL ARCHIVO ZARR: {args.zarr_path}")
|
||||||
|
print("="*60)
|
||||||
|
ds.info()
|
||||||
|
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print(" VARIABLES DISPONIBLES PARA GRAFICAR (-v):")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
# Mostramos las variables y sus dimensiones para orientar al usuario
|
||||||
|
for var in ds.data_vars:
|
||||||
|
dims = ds[var].dims
|
||||||
|
print(f" - {var} {list(dims)}")
|
||||||
|
|
||||||
|
print("\nEjemplo de uso:")
|
||||||
|
print(f" zarrviewer {args.zarr_path} -v {list(ds.data_vars)[0]}")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# SECCIÓN DE TIEMPOS
|
||||||
|
# =====================================================================
|
||||||
|
posibles_nombres = ["time", "valid_time", "datetime", "t", "Time", "Valid_time"]
|
||||||
|
time_coord = None
|
||||||
|
|
||||||
|
if args.coord:
|
||||||
|
if args.coord in ds.coords:
|
||||||
|
time_coord = args.coord
|
||||||
|
else:
|
||||||
|
print(f"La coordenada '{args.coord}' no existe. Disponibles: {list(ds.coords.keys())}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
for nombre in posibles_nombres:
|
||||||
|
if nombre in ds.coords:
|
||||||
|
time_coord = nombre
|
||||||
|
break
|
||||||
|
|
||||||
|
if time_coord is None:
|
||||||
|
print("No se encontró una coordenada temporal.", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
time_dim = ds[time_coord].dims[0]
|
||||||
|
times = ds[time_coord].values
|
||||||
|
total = len(times)
|
||||||
|
|
||||||
|
print(f"Usando coordenada temporal: '{time_coord}' (Eje dimensional: '{time_dim}')")
|
||||||
|
print(f"Total de frames en el Zarr: {total}\n")
|
||||||
|
|
||||||
|
def format_time(t):
|
||||||
|
return pd.to_datetime(t).strftime("%Y-%m-%d %H:%M")
|
||||||
|
|
||||||
|
if args.all:
|
||||||
|
for t in times:
|
||||||
|
print(format_time(t))
|
||||||
|
else:
|
||||||
|
first_n = min(args.first, total)
|
||||||
|
last_n = min(args.last, total)
|
||||||
|
for t in times[:first_n]:
|
||||||
|
print(format_time(t))
|
||||||
|
if total > first_n + last_n:
|
||||||
|
print("\n ... \n")
|
||||||
|
for t in times[-last_n:]:
|
||||||
|
print(format_time(t))
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# SECCIÓN DE GRAFICADO INTELIGENTE
|
||||||
|
# =====================================================================
|
||||||
|
if args.variable:
|
||||||
|
print("\n" + "="*60)
|
||||||
|
|
||||||
|
match = re.match(r"^([a-zA-Z0-9_]+)(?:[\(\:\.](.+?)\)?)?$", args.variable)
|
||||||
|
if not match:
|
||||||
|
print(f"[ERROR] Sintaxis inválida para -v. Usa 'var', 'var:dim', 'var.dim' o 'var(dim)'", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
var_name = match.group(1)
|
||||||
|
dim_val_request = match.group(2)
|
||||||
|
|
||||||
|
if var_name not in ds.data_vars:
|
||||||
|
print(f"[ERROR] La variable '{var_name}' no existe.", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f" PROCESANDO VARIABLE: '{var_name}'")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
var_dims = list(ds[var_name].dims)
|
||||||
|
other_dims = [d for d in var_dims if d != time_dim]
|
||||||
|
|
||||||
|
try:
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
# CASO A: Mapa (2D o más)
|
||||||
|
if len(other_dims) >= 2:
|
||||||
|
ultimo_time_str = format_time(times[-1])
|
||||||
|
print(f"-> Detectado formato de mapa (Dimensiones: {other_dims}).")
|
||||||
|
print(f"-> Extrayendo último frame ({ultimo_time_str})...")
|
||||||
|
|
||||||
|
ds_plot = ds[var_name].isel({time_dim: -1})
|
||||||
|
plot_kwargs = {}
|
||||||
|
|
||||||
|
# Búsqueda oportunista de georreferencia
|
||||||
|
lat_name, lon_name = None, None
|
||||||
|
for lat in ['lat', 'latitude', 'XLAT', 'xlat']:
|
||||||
|
if lat in ds.variables:
|
||||||
|
lat_name = lat
|
||||||
|
break
|
||||||
|
for lon in ['lon', 'longitude', 'XLONG', 'xlon']:
|
||||||
|
if lon in ds.variables:
|
||||||
|
lon_name = lon
|
||||||
|
break
|
||||||
|
|
||||||
|
if lat_name and lon_name:
|
||||||
|
print(f"-> Georreferencia detectada: '{lat_name}', '{lon_name}'. Aplicando ejes...")
|
||||||
|
lat_arr = ds[lat_name].isel({time_dim: -1}) if time_dim in ds[lat_name].dims else ds[lat_name]
|
||||||
|
lon_arr = ds[lon_name].isel({time_dim: -1}) if time_dim in ds[lon_name].dims else ds[lon_name]
|
||||||
|
|
||||||
|
ds_plot = ds_plot.assign_coords({lat_name: lat_arr, lon_name: lon_arr})
|
||||||
|
plot_kwargs['x'] = lon_name
|
||||||
|
plot_kwargs['y'] = lat_name
|
||||||
|
|
||||||
|
plt.figure(figsize=(8, 10))
|
||||||
|
cmap_choice = 'Blues' if var_name in ['tp', 'sd', 'snowc', 'RAINNC', 'RAINC'] else 'viridis'
|
||||||
|
plot_kwargs['cmap'] = cmap_choice
|
||||||
|
|
||||||
|
# Achicar la barra de colores a la mitad
|
||||||
|
plot_kwargs['cbar_kwargs'] = {'shrink': 0.5}
|
||||||
|
|
||||||
|
ds_plot.plot(**plot_kwargs)
|
||||||
|
|
||||||
|
# Mantener proporción georeferenciada 1:1
|
||||||
|
plt.gca().set_aspect('equal')
|
||||||
|
|
||||||
|
plt.title(f"{var_name} | Frame: {ultimo_time_str}")
|
||||||
|
nombre_imagen = f"mapa_{var_name}_{pd.to_datetime(times[-1]).strftime('%Y%m%d_%H%M')}.png"
|
||||||
|
|
||||||
|
# CASO B: Puntos / Estaciones (1D adicional)
|
||||||
|
elif len(other_dims) == 1:
|
||||||
|
dim_name = other_dims[0]
|
||||||
|
dim_values = ds[dim_name].values
|
||||||
|
|
||||||
|
if dim_val_request is None:
|
||||||
|
print(f"Se detectó que la variable '{var_name}' está asociada a la dimensión: '{dim_name}'")
|
||||||
|
print(f"Valores posibles para {dim_name}:")
|
||||||
|
# Imprimir valores con salto de línea para facilitar el uso
|
||||||
|
print(", ".join(map(str, dim_values)) + "\n")
|
||||||
|
|
||||||
|
print("Ejemplos de uso (cópialos y pégalos):")
|
||||||
|
for ex in dim_values[:3]:
|
||||||
|
print(f" zarrviewer {args.zarr_path} -v {var_name}:{ex}")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
try:
|
||||||
|
target_type = type(dim_values[0])
|
||||||
|
casted_val = target_type(dim_val_request)
|
||||||
|
except ValueError:
|
||||||
|
casted_val = dim_val_request
|
||||||
|
|
||||||
|
if casted_val not in dim_values:
|
||||||
|
print(f"[ERROR] El valor '{casted_val}' no existe en la dimensión '{dim_name}'.", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"-> Detectada serie de tiempo para {dim_name} = {casted_val}")
|
||||||
|
print(f"-> Extrayendo los últimos {args.npoints} registros...")
|
||||||
|
|
||||||
|
ds_plot = ds[var_name].sel({dim_name: casted_val}).isel({time_dim: slice(-args.npoints, None)})
|
||||||
|
|
||||||
|
plt.figure(figsize=(10, 5))
|
||||||
|
ds_plot.plot.line(marker='o', linestyle='-', color='#1f77b4')
|
||||||
|
plt.title(f"Serie de tiempo: {var_name} ({dim_name}: {casted_val})")
|
||||||
|
plt.grid(True, linestyle='--', alpha=0.6)
|
||||||
|
|
||||||
|
nombre_imagen = f"serie_{var_name}_{casted_val}.png"
|
||||||
|
|
||||||
|
# CASO C: Serie de tiempo global (0 dimensiones)
|
||||||
|
else:
|
||||||
|
print(f"-> Detectada serie de tiempo unidimensional global.")
|
||||||
|
print(f"-> Extrayendo los últimos {args.npoints} registros...")
|
||||||
|
|
||||||
|
ds_plot = ds[var_name].isel({time_dim: slice(-args.npoints, None)})
|
||||||
|
|
||||||
|
plt.figure(figsize=(10, 5))
|
||||||
|
ds_plot.plot.line(marker='o')
|
||||||
|
plt.title(f"Serie global: {var_name}")
|
||||||
|
plt.grid(True, linestyle='--', alpha=0.6)
|
||||||
|
|
||||||
|
nombre_imagen = f"serie_global_{var_name}.png"
|
||||||
|
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.savefig(nombre_imagen, dpi=150)
|
||||||
|
plt.close()
|
||||||
|
|
||||||
|
print(f"-> [ÉXITO] Gráfico generado correctamente.")
|
||||||
|
print(f"-> Archivo guardado en: {os.path.abspath(nombre_imagen)}")
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
print("[ERROR] Faltan librerías. Asegúrate de instalar 'matplotlib'", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] No se pudo generar el gráfico debido a: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user