Python científico y graficación · Licenciatura avanzada / posgrado inicial · 8 horas
NumPy para estados, trayectorias y mallas
Formas, tipos, vistas, broadcasting, álgebra lineal y mallas aplicadas a datos de sistemas dinámicos.
Forma de los arreglos y convención del modelo
NumPy es una biblioteca pública para cálculo con arreglos. La instrucción
import numpy as np la carga bajo el alias convencional
np. Primero se construye un vector pequeño y se inspecciona:
import numpy as np
state = np.array([1.0, 0.0], dtype=np.float64)
print(state)
print(state.shape) # (2,)
print(state.dtype) # float64
np.array(datos, dtype=tipo) recibe los datos como primer argumento
y el tipo como argumento nombrado. shape y dtype son
atributos del arreglo, por eso se consultan sin paréntesis. En cambio,
state.copy() es un método y sí se llama con paréntesis.
NumPy no adivina qué representa cada eje. Una trayectoria con tiempos y variables puede almacenarse como o como ; ambas convenciones son válidas, pero mezclarlas produce resultados numéricamente plausibles y conceptualmente falsos. En los ejemplos que siguen, cada fila será un tiempo y cada columna una variable:
times = np.linspace(0.0, 40.0, 8001, dtype=np.float64)
states = np.empty((times.size, 3), dtype=np.float64)
states[0] = (1.0, 1.0, 1.0)
assert states.shape == (8001, 3)
assert times.shape == (8001,)
La guía inicial oficial de NumPy explica dimensiones, forma, tamaño, tipo y operaciones por ejes. Conviene predecir la forma de cada expresión antes de ejecutarla. El código debe poder responder: ¿qué eje enumera tiempos?, ¿cuál variables?, ¿qué unidad tiene cada columna?
np.linspace(inicio, fin, cantidad) crea muestras equiespaciadas e
incluye ambos extremos. np.empty(forma) reserva memoria sin fijar
sus valores; por eso debe llenarse antes de leerse. Estas funciones proceden de
NumPy y tienen un contrato documentado. La función
validate_trajectory del apartado siguiente será código propio y
añadirá las comprobaciones científicas de este curso.
Seleccionar states[:, 0] produce una serie de longitud .
Seleccionar states[2000:] descarta un transitorio por índice, pero
solo representa un tiempo físico si se conoce el muestreo. Es más seguro
calcular una máscara:
retained = states[times >= 10.0]
Así, cambiar el paso o el número de muestras no cambia inadvertidamente el transitorio físico.
Práctica interactiva. En una celda de Jupyter o del navegador, crea los
arreglos [1, 2, 3] y [[1, 2, 3], [4, 5, 6]]. Predice
ndim, shape, size y el resultado de sumar
10 antes de ejecutar. Transpón la matriz y explica por qué cambian los ejes,
pero no el número de elementos. El ejercicio usa solo NumPy y no requiere
archivos ni hardware externo.
Tipos, finitud y escala
El tipo determina precisión y memoria. Una trayectoria de diez millones de estados tridimensionales ocupa aproximadamente MB en precisión doble y MB en precisión simple. Reducir memoria no es gratis: puede cambiar el horizonte antes de que el redondeo domine una dinámica sensible.
Se valida antes de analizar:
def validate_trajectory(times, states, dimension):
times = np.asarray(times, dtype=np.float64)
states = np.asarray(states, dtype=np.float64)
if times.ndim != 1:
raise ValueError("times debe ser un vector")
if states.shape != (times.size, dimension):
raise ValueError("states debe tener forma (N, dimension)")
if not np.isfinite(times).all() or not np.isfinite(states).all():
raise FloatingPointError("Hay NaN o infinito")
if not np.all(np.diff(times) > 0):
raise ValueError("El tiempo no es estrictamente creciente")
return times, states
La conversión no repara el experimento; normaliza el contrato. Un infinito puede señalar paso inadecuado, parámetros erróneos o escape real. Se registra la causa antes de eliminar la fila.
Vistas, copias y cambios silenciosos
Muchas rebanadas son vistas de la misma memoria:
x_view = states[:, 0]
x_copy = states[:, 0].copy()
x_view[0] = 99.0
La primera asignación también cambia states[0, 0]; la copia
permanece independiente. Esta propiedad ahorra memoria, pero puede corromper
datos primarios si una función “limpia” una serie en sitio. Una función de
análisis debería documentar si modifica la entrada. Por defecto, esta ruta
conserva los datos y devuelve un resultado nuevo.
El indexado booleano suele crear copia:
mask = times >= 10.0
tail = states[mask]
No se memoriza una tabla aislada de reglas. Se usa
np.shares_memory cuando la independencia sea importante y se
añade una prueba que asegure que el análisis no altera la trayectoria.
Broadcasting como álgebra sobre estados
El broadcasting oficial
alinea dimensiones desde la derecha. Si states tiene forma
y equilibrium forma , la resta
equilibrium = np.array([0.0, 0.0, 0.0])
displacements = states - equilibrium
distances = np.linalg.norm(displacements, axis=1)
trata el equilibrio como una fila que se aplica a todos los tiempos. El
argumento axis=1 calcula una norma por estado; usar
axis=0 produciría una norma por variable. El resultado puede tener
números razonables en ambos casos, por lo que la prueba debe revisar también la
forma esperada .
Para comparar condiciones iniciales con equilibrios:
delta = initial[:, None, :] - equilibria[None, :, :]
distance = np.linalg.norm(delta, axis=-1)
assert distance.shape == (M, K)
nearest = np.argmin(distance, axis=1)
Los ejes de longitud uno no almacenan copias completas; permiten que NumPy combine formas compatibles. Sin embargo, el arreglo resultado sí puede ser enorme. Si y crecen, se procesa por bloques.
Representación columnar de un oscilador
Considérese una trayectoria del oscilador amortiguado con columnas
y masa unitaria. La energía mecánica instantánea es
. Si states tiene forma , el
cálculo mantiene una salida por tiempo:
x = states[:, 0]
v = states[:, 1]
k = 4.0
energy = 0.5 * v**2 + 0.5 * k * x**2
assert energy.shape == times.shape
La aserción no es decorativa. Si se hubiera almacenado la trayectoria como , las mismas rebanadas seleccionarían dos instantes completos y el resultado tendría longitud dos. La fórmula seguiría ejecutándose porque NumPy opera con cualquier forma compatible; solo el significado físico revelaría el error. Por eso una interfaz de datos debe declarar tanto el nombre como la posición de cada eje.
Para un sistema sin amortiguamiento, la variación relativa ayuda a revisar el integrador. Para el sistema amortiguado no se exige energía constante: se verifica que su tendencia concuerde con . Una pequeña subida aislada puede proceder del error de discretización; una subida sostenida sugiere signo equivocado, paso excesivo o columnas intercambiadas. El arreglo no decide cuál explicación es correcta, pero permite formular indicadores que separen esas posibilidades.
El tiempo merece el mismo cuidado que el estado. La expresión
np.diff(times) produce los anchos de intervalos. Si todos
coinciden dentro de tolerancia, una FFT puede asociarse con una frecuencia de
muestreo única; si el integrador entregó una malla adaptativa, aplicar una FFT
directamente sería conceptualmente incorrecto aunque el programa aceptara el
arreglo. En ese caso se evalúa la solución densa sobre una malla uniforme o se
elige un análisis adecuado para muestreo irregular.
También hay que distinguir normalización numérica de cambio de unidades. Restar la media y dividir por la desviación estándar puede ser útil para una comparación estadística, pero ya no deja coordenadas en metros y metros por segundo. Se conserva el arreglo original y se devuelve, junto con el normalizado, el desplazamiento y la escala usados. Así puede invertirse la transformación y el gráfico puede etiquetar honestamente sus ejes.
Supongamos que se tienen trayectorias del mismo oscilador con forma . La energía se calcula sin copiar coeficientes sobre el tiempo:
weights = np.array([k, 1.0])
ensemble_energy = 0.5 * np.sum(weights * trajectories**2, axis=-1)
assert ensemble_energy.shape == (M, N)
Aquí el broadcasting aplica dos pesos sobre el último eje y la suma elimina
precisamente ese eje. Escribir axis=1 mezclaría tiempos; el tamaño
resultante podría parecer correcto para algún caso cuadrado. Una prueba con
evita que la coincidencia accidental de dimensiones oculte el
defecto.
Mallas y conservación de ejes
Una sección bidimensional de condiciones iniciales se construye así:
x0 = np.linspace(-2.0, 2.0, 161)
v0 = np.linspace(-2.0, 2.0, 121)
X0, V0 = np.meshgrid(x0, v0, indexing="xy")
initial_grid = np.stack((X0, V0), axis=-1)
assert X0.shape == (121, 161)
assert initial_grid.shape == (121, 161, 2)
El primer eje de la malla corresponde a y el segundo a . Para un integrador que recibe una lista de estados se aplana solo la parte espacial:
flat_initial = initial_grid.reshape(-1, 2)
labels_flat = classify_many(flat_initial)
labels = labels_flat.reshape(V0.shape)
Se conserva la forma original para reconstruir la imagen. Invertir ejes, transponer etiquetas o usar una extensión gráfica incongruente puede reflejar la cuenca respecto de la diagonal sin producir error de Python.
Atlas reproducible de sistemas dinámicos Función: plot_bistable_basin
Se muestra el código fuente completo en Python. Las funciones indicadas arriba producen esta figura; el archivo declara las bibliotecas requeridas e incluye las funciones auxiliares. Este control no ejecuta código en el servidor.
El script se cargará al abrir este panel. El experimento de cuencas se inspira en el tratamiento geométrico de Tél y Gruiz Tél, 2006 Tamás Tél y Márton Gruiz (2006) Chaotic Dynamics: An Introduction Based on Classical Mechanics Cambridge University Press Ubicación consultada: §§1.2.2 y 6.8 . Esa obra aporta la pregunta dinámica; NumPy aporta la representación y las operaciones con las que se ejecuta.
Álgebra lineal para lotes de jacobianos
La referencia oficial de álgebra lineal incluye solución de sistemas, autovalores, normas y descomposiciones. Para clasificar un equilibrio de un sistema bidimensional:
jacobian = np.array([[0.0, 1.0], [1.0, -0.3]])
eigenvalues = np.linalg.eigvals(jacobian)
Los autovalores no se ordenan de manera científicamente significativa. Se clasifican por partes reales, no por posición en el arreglo. Para una familia de jacobianos con forma , varias rutinas aceptan los dos últimos ejes como matrices:
eigenvalues = np.linalg.eigvals(jacobians)
assert eigenvalues.shape == (M, d)
max_real = eigenvalues.real.max(axis=1)
Un valor cercano a cero requiere tolerancia y control del parámetro. No se redondea primero para decidir estabilidad. En matrices no normales, los autovalores tampoco describen todo crecimiento transitorio; la interpretación se limita al criterio calculado.
Para resolver se usa
np.linalg.solve(A, b), no se forma la inversa. Se comprueba el
residuo y el condicionamiento. Un residuo pequeño
no garantiza error pequeño cuando está mal condicionada.
Vectorización y legibilidad del algoritmo
Vectorizar elimina bucles de Python cuando todos los elementos siguen la misma operación. No convierte automáticamente un método en correcto ni más legible. Una primera versión explícita puede servir como referencia:
reference = np.array([
np.linalg.norm(state - equilibrium)
for state in states
])
vectorized = np.linalg.norm(states - equilibrium, axis=1)
np.testing.assert_allclose(vectorized, reference)
Después de validar, se mide tiempo y memoria. Una expresión compacta que crea varios arreglos temporales gigantes puede ser peor que un bucle por bloques. La elección se documenta con el tamaño real del experimento.
Práctica ejecutable
Forma y error de una malla NumPy
Duplica el número de puntos y compara la forma, el paso y el error máximo de la identidad trigonométrica.
Listo para ejecutar.
Salida
Todavía no se ha ejecutado el programa.
Ejecución, privacidad y límites
El programa se ejecuta en un Web Worker dentro de este navegador. No usa la Raspberry Pi ni un proceso Python del servidor de Fyskode. Al pulsar Ejecutar por primera vez, el navegador descarga Pyodide y las bibliotecas importadas desde el CDN oficial de jsDelivr; esas solicitudes siguen la política de privacidad del CDN. El código escrito aquí no se envía a Fyskode.
Cada ejecución dispone de hasta 60 segundos, 200 000 caracteres de salida y seis gráficas; las figuras excesivamente grandes se omiten. El tiempo se cuenta después de cargar el motor y las bibliotecas. Detener o agotar el tiempo elimina el Worker y su memoria. El entorno no tiene acceso directo a los archivos del equipo, pero un programa puede solicitar recursos de red si se le ordena; no pegues contraseñas ni credenciales.
Ensayos de forma, memoria y ejes
- Para una trayectoria , calcula media, mínimo y máximo por variable.
- Demuestra con
np.shares_memoryla diferencia entre vista y copia. - Predice la forma de tres operaciones de broadcasting y confírmala con aserciones.
- Resuelve un sistema lineal y compara residuo con condicionamiento.
- Procesa distancias por bloques y comprueba que coincidan con la versión vectorizada completa.
Cada arreglo del informe debe poder describirse con una frase científica y una tupla de formas. Si ambas descripciones no coinciden, todavía no es seguro usarlo en una figura o en una conclusión cuantitativa.
Fuentes consultadas
Obras citadas en el desarrollo; los localizadores indican los capítulos o secciones consultados.Kathleen T. Alligood, Tim D. Sauer y James A. Yorke (1996). Chaos: An Introduction to Dynamical Systems. Springer. ISBN 0-387-94677-2.
§§3.5, 4.4 y visitas de laboratorio · Experimentos de cuencas y geometría computacional adaptados.Tamás Tél y Márton Gruiz (2006). Chaotic Dynamics: An Introduction Based on Classical Mechanics. Cambridge University Press. ISBN 978-0-521-54783-3.
§§1.2.2 y 6.8 · Mallas de condiciones iniciales y fronteras de cuencas.