Fyskode Learning

Python científico y graficación · Licenciatura avanzada / posgrado inicial · 7 horas

Fundamentos de Python para cálculo científico

Ejecución, variables, tipos, colecciones, decisiones, ciclos, funciones, módulos, archivos y una primera aplicación reproducible.

Ejecución y primeros cálculos

Python ejecuta instrucciones en el orden escrito. Para comenzar, crea un archivo llamado primer_calculo.py con tres operaciones sencillas:

temperature_c = 22.0
temperature_k = temperature_c + 273.15

print("Temperatura en kelvin:", temperature_k)

Desde la carpeta del archivo, ejecuta python primer_calculo.py. La primera línea asigna un número a un nombre; la segunda calcula un valor nuevo; la tercera muestra el resultado. Antes de introducir arreglos, integradores o sistemas caóticos, conviene dominar este ciclo corto: escribir, ejecutar, leer la salida y corregir.

La guía oficial del lenguaje es la fuente primaria para la sintaxis usada en esta unidad. No se presupone otro tutorial: aquí se estudian variables, tipos, colecciones, decisiones, ciclos, funciones, módulos, excepciones y archivos Foundation, 2026 Python Software Foundation (2026) The Python Tutorial Documentación de Python 3 · Python Software Foundation Ubicación consultada: §§3–8: introducción informal, control de flujo, estructuras de datos, módulos, entrada/salida y errores Abrir fuente .

Al final se organiza una primera aplicación científica. Esa aplicación sirve para reunir lo aprendido; no es el punto de partida ni exige comprender aún los detalles del integrador, que se desarrollan en la unidad 3.

Los ejemplos iniciales usan únicamente Python 3 y pueden ejecutarse en una terminal, una celda de Jupyter o un intérprete de Python en el navegador. No requieren hardware externo. Las instrucciones de entorno virtual y archivos se practican después en una computadora porque trabajan con carpetas y paquetes.

Variables, tipos y semántica de los datos

Una asignación vincula un nombre con un objeto:

dt = 0.01
steps = 8_000
system_name = "lorenz"
discard_transient = True
initial_state = None

Aquí aparecen float, int, str, bool y None. Python determina el tipo del objeto al ejecutar la asignación; eso no significa que todas las operaciones sean válidas. steps + 1 tiene sentido, mientras que system_name + 1 produce un TypeError. La función type(value) permite inspeccionar un objeto, aunque en código científico suele ser más útil validar la propiedad necesaria: que el paso sea real y positivo, que el número de pasos sea entero y que el nombre no esté vacío.

Las conversiones deben expresar una decisión, no tapar una entrada defectuosa:

raw_steps = "8000"
steps = int(raw_steps)       # válido porque la cadena representa un entero
duration = steps * float(dt)

if steps <= 0:
    raise ValueError("steps debe ser positivo")

= asigna; == compara valores. La identidad is pregunta si dos nombres apuntan al mismo objeto y se reserva sobre todo para None: initial_state is None. Usar is para comparar números o cadenas confunde identidad con igualdad.

También importa la mutabilidad. Números, cadenas y tuplas no se modifican en el lugar; listas y diccionarios sí. Si dos nombres apuntan a la misma lista, una modificación será visible desde ambos:

state = [1.0, 1.0, 1.0]
alias = state
alias[0] = 1.000001
assert state[0] == 1.000001

independent_state = state.copy()

Esta diferencia explica muchos errores al preparar condiciones iniciales. Una segunda corrida debe recibir una copia deliberada, no un alias accidental.

Colecciones para estados, parámetros y resultados

Las colecciones no son intercambiables; cada una comunica una relación distinta.

  • Una lista mantiene orden y admite cambios: samples = [0.0, 0.1, 0.2].
  • Una tupla conserva una agrupación de tamaño fijo: t_span = (0.0, 80.0).
  • Un diccionario asocia claves con valores: params = {"sigma": 10.0, "rho": 28.0, "beta": 8/3}.
  • Un conjunto guarda elementos únicos: required = {"sigma", "rho", "beta"}.

Los índices comienzan en cero. state[0] es la primera coordenada y state[-1] la última. Un corte como state[:2] produce los dos primeros elementos sin incluir el extremo derecho. Para estados pequeños, desempaquetar hace visible el significado:

x, y, z = state
sigma = params["sigma"]
missing = required - params.keys()

if missing:
    raise ValueError(f"Faltan parámetros: {sorted(missing)}")

Acceder con params["sigma"] falla si la clave no existe, lo cual es apropiado cuando el parámetro es obligatorio. params.get("method", "RK45") expresa, en cambio, que la ausencia tiene un valor predeterminado. Mezclar ambas políticas sin decidir cuál corresponde vuelve silenciosa una configuración incompleta.

Las comprensiones sirven para transformaciones breves y legibles:

positive_values = [value for value in samples if value > 0]
squared = {name: value**2 for name, value in params.items()}

Si la condición, el cálculo o el manejo de errores necesitan varias líneas, un ciclo explícito se entiende mejor que una comprensión anidada.

Condicionales y ciclos con criterio científico

if, elif y else seleccionan una rama. La sangría forma parte de la sintaxis:

if dt <= 0:
    raise ValueError("dt debe ser positivo")
elif dt > 0.1:
    print("Advertencia: comprueba convergencia con un paso menor")
else:
    print("Paso admitido; todavía falta verificar convergencia")

La última frase evita una conclusión frecuente: superar una validación de rango no demuestra que la discretización sea adecuada.

Un ciclo for recorre una colección. range(n) genera los enteros desde cero hasta n-1; enumerate añade el índice y zip recorre secuencias en paralelo:

coordinates = ["x", "y", "z"]
state = [1.0, 1.0, 1.0]

for index, (name, value) in enumerate(zip(coordinates, state)):
    print(index, name, value)

Conviene comprobar antes que las secuencias tengan la misma longitud, porque zip termina al agotarse la más corta. Un while corresponde a una condición de parada que no se conoce de antemano, pero necesita una cota adicional:

iteration = 0
residual = float("inf")
max_iterations = 100

while residual > 1e-10 and iteration < max_iterations:
    residual = update_once()
    iteration += 1

if residual > 1e-10:
    raise RuntimeError("No hubo convergencia en 100 iteraciones")

Sin max_iterations, un criterio mal planteado puede dejar el experimento en un ciclo infinito. En cálculo numérico deben registrarse tanto la tolerancia como el límite de iteraciones.

Funciones con entradas y salidas verificables

Una función reúne una operación con sus datos de entrada y su resultado. La palabra return entrega el valor al código que llamó a la función:

def validate_time_grid(t_span: tuple[float, float], dt: float) -> int:
    """Valida el intervalo y devuelve el número de pasos completos."""
    t0, tf = t_span
    if tf <= t0:
        raise ValueError("t_span debe satisfacer tf > t0")
    if dt <= 0:
        raise ValueError("dt debe ser positivo")

    steps = round((tf - t0) / dt)
    if not steps:
        raise ValueError("El intervalo no contiene un paso completo")
    return steps

Los argumentos posicionales funcionan bien cuando el significado es inmediato; los argumentos con nombre evitan intercambios en llamadas largas:

n = validate_time_grid(t_span=(0.0, 80.0), dt=0.01)

Una función fácil de probar no depende de variables globales, no abre archivos sin anunciarlo y devuelve lo que calcula. Los valores predeterminados también merecen cuidado: una lista mutable no debe usarse como predeterminado porque se reutilizaría entre llamadas. Se usa None y se crea la lista dentro:

def add_note(text: str, notes: list[str] | None = None) -> list[str]:
    result = [] if notes is None else notes.copy()
    result.append(text)
    return result

La sintaxis general es def nombre(parámetros):; el bloque sangrado forma el cuerpo y return entrega el resultado. En validate_time_grid(t_span=(0.0, 80.0), dt=0.01), el nombre de la función va antes de los paréntesis y los argumentos concretos van dentro. Las funciones validate_time_grid y add_note son funciones propias: el estudiante puede leer y modificar su implementación. En cambio, print, len, range y type son funciones incorporadas y documentadas por Python.

Práctica interactiva. Copia en una sola celda las dos funciones anteriores. Ejecuta tres llamadas válidas y tres inválidas; antes de cada llamada predice el valor o la excepción. Después escribe mean(values) sin importar ninguna biblioteca, observa el NameError y programa tu propia función mean con sum y len. Esta secuencia distingue con claridad definir, llamar, devolver y manejar un fallo.

Módulos, excepciones y archivos

Un módulo es un archivo .py. La instrucción import carga sus definiciones y conserva el espacio de nombres:

from pathlib import Path
import json
import math

root = Path("results")
scale = math.sqrt(2.0)

math, json y pathlib pertenecen a la biblioteca estándar: acompañan al intérprete y no se instalan con pip. La instrucción import math carga el módulo y math.sqrt(2.0) llama a su función sqrt. Mantener el prefijo del módulo deja visible la procedencia de la operación. NumPy, SciPy, Matplotlib y Hidden Attractors FO son paquetes externos; se instalarán y se usarán progresivamente en las unidades siguientes.

En un proyecto, from model import lorenz comunica de qué módulo procede la función. from model import * oculta esa procedencia y puede sobrescribir nombres sin que la lectura lo revele. Cuando varios archivos forman un paquete, una carpeta con módulos y una interfaz pequeña permite reutilizar el modelo desde pruebas, scripts y cuadernos.

Las excepciones separan el recorrido normal de una entrada o estado que impide continuar. Se captura únicamente lo que puede explicarse o corregirse en ese nivel. Este lector distingue archivo ausente, JSON inválido y configuración semánticamente incorrecta:

from pathlib import Path
import json

def load_config(path: Path) -> dict[str, object]:
    try:
        with path.open("r", encoding="utf-8") as handle:
            data = json.load(handle)
    except FileNotFoundError as error:
        raise FileNotFoundError(f"No existe la configuración: {path}") from error
    except json.JSONDecodeError as error:
        raise ValueError(
            f"JSON inválido en {path}, línea {error.lineno}, columna {error.colno}"
        ) from error

    if not isinstance(data, dict):
        raise ValueError("La raíz de la configuración debe ser un objeto JSON")
    return data

with cierra el archivo incluso si ocurre un error. Declarar encoding="utf-8" evita que el resultado dependa de la codificación predeterminada del equipo. pathlib representa rutas sin concatenar manualmente barras. Para escribir un resultado sin sobrescribirlo por accidente:

def save_summary(path: Path, summary: dict[str, object]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("x", encoding="utf-8") as handle:
        json.dump(summary, handle, indent=2, sort_keys=True)
        handle.write("\n")

El modo "x" falla si el archivo ya existe. Cambiarlo por "w" autoriza una sobrescritura; esa elección pertenece al diseño del experimento. Un bloque except Exception: que ignore todo impediría distinguir un archivo repetido de un error de programación, por lo que no se usa como salida rápida.

Los apartados siguientes reúnen estas construcciones en una aplicación integradora. Si estás comenzando, verifica primero los ejemplos breves de variables, colecciones, control de flujo, funciones y archivos. Después ejecuta el proyecto por etapas y conserva una salida comprobable en cada etapa.

Conviene distinguir tres niveles:

  • Código fuente: funciones que implementan el modelo y el procedimiento.
  • Configuración: valores que cambian entre ejecuciones.
  • Resultados: datos y figuras generados, acompañados por un manifiesto.

Si un parámetro aparece escondido dentro de una función de dibujo, no puede auditarse con facilidad. Si una celda del cuaderno depende de haber ejecutado otra fuera de orden, el estado real del experimento queda oculto. El cuaderno sirve para explorar; el módulo y el comando reproducible conservan el cálculo.

Entorno virtual y aislamiento de dependencias

Un entorno virtual separa las bibliotecas del proyecto de las instaladas globalmente. La referencia es la sección oficial sobre entornos virtuales y paquetes. En PowerShell:

py -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install numpy scipy matplotlib

Usar python -m pip reduce la posibilidad de instalar con un intérprete y ejecutar con otro. Después se comprueba:

python -c "import sys; print(sys.executable)"
python -c "import numpy, scipy, matplotlib; print(numpy.__version__, scipy.__version__, matplotlib.__version__)"

El archivo de dependencias debe expresar una decisión. Congelar absolutamente todo con pip freeze reproduce con precisión un entorno concreto, pero incluye dependencias transitivas. Declarar solo NumPy, SciPy y Matplotlib comunica mejor las dependencias directas, aunque permite cambios futuros. Un proyecto académico puede conservar ambos: un archivo con dependencias directas y otro con las versiones resueltas.

No se guarda la carpeta del entorno en el repositorio. Se guarda el comando que la reconstruye, la versión de Python y la lista de paquetes. Si una ejecución se realizó con otra versión, el manifiesto debe decirlo.

Estructura de carpetas por responsabilidad

Una estructura suficiente para comenzar es:

experimento_lorenz/
  README.md
  requirements.in
  requirements-lock.txt
  configs/
    baseline.json
  src/
    model.py
    simulate.py
    plot.py
  tests/
    test_model.py
  results/

El README responde cuatro preguntas inmediatas: cómo crear el entorno, cómo ejecutar una configuración, dónde aparecen los resultados y cómo correr las pruebas. El módulo del modelo no abre archivos ni dibuja; el de simulación coordina la integración; el de figuras transforma datos guardados.

La organización sigue la documentación oficial de módulos. Importar una función no debería iniciar una simulación. La ejecución vive bajo un punto de entrada:

def main() -> None:
    config = load_config("configs/baseline.json")
    result = run_experiment(config)
    save_result(result, "results/baseline")

if __name__ == "__main__":
    main()

Esta condición permite reutilizar y probar las funciones sin producir archivos por accidente.

El modelo como función explícita

Los parámetros pueden representarse con una clase de datos inmutable:

from dataclasses import dataclass
import numpy as np
from numpy.typing import NDArray

Vector = NDArray[np.float64]

@dataclass(frozen=True)
class LorenzParameters:
    sigma: float = 10.0
    rho: float = 28.0
    beta: float = 8.0 / 3.0

def lorenz(t: float, state: Vector, p: LorenzParameters) -> Vector:
    state = np.asarray(state, dtype=np.float64)
    if state.shape != (3,):
        raise ValueError(f"Se esperaba state.shape == (3,), no {state.shape}")
    x, y, z = state
    return np.array([
        p.sigma * (y - x),
        x * (p.rho - z) - y,
        x * y - p.beta * z,
    ])

El argumento temporal aparece aunque el campo sea autónomo porque ese contrato también admite sistemas forzados. Convertir el estado a precisión doble vuelve explícito el tipo esperado. La comprobación de forma falla cerca de la causa: una trayectoria transpuesta no llega silenciosamente al integrador.

Las ecuaciones proceden del modelo de Lorenz, cuya sensibilidad sirve como experimento conductor Lorenz, 1993 Edward N. Lorenz (1993) The Essence of Chaos University of Washington Press Ubicación consultada: caps. 1–3 . La referencia científica justifica el sistema y los parámetros clásicos; la documentación de Python justifica cómo se organiza el programa.

Validación de la configuración

Un JSON base puede contener:

{
  "system": "lorenz",
  "parameters": {"sigma": 10.0, "rho": 28.0, "beta": 2.6666666666666665},
  "initial_state": [1.0, 1.0, 1.0],
  "t_span": [0.0, 80.0],
  "transient": 20.0,
  "method": "RK45",
  "rtol": 1e-9,
  "atol": 1e-11
}

Leer JSON produce datos, no una configuración confiable. Se comprueba que el tiempo final sea mayor que el inicial, que el transitorio pertenezca al intervalo, que el estado tenga tres componentes y que las tolerancias sean positivas. Rechazar una entrada ambigua es mejor que corregirla en silencio.

Las rutas se manejan con pathlib.Path, no concatenando separadores:

from pathlib import Path

output = Path("results") / "baseline"
output.mkdir(parents=True, exist_ok=False)

Impedir la reutilización accidental de una carpeta evita sobrescribir una ejecución sin advertencia. Si el proyecto permite regenerarla, esa política debe ser deliberada.

Ejecución reproducible del sistema de Lorenz

Antes de integrar durante ochenta unidades de tiempo conviene verificar un instante que pueda resolverse sin software. Para el estado inicial (x,y,z)=(1,1,1)(x,y,z)=(1,1,1) y los parámetros clásicos, la primera componente del campo es 10(11)=010(1-1)=0; la segunda es 1(281)1=261(28-1)-1=26; la tercera es 1(8/3)=5/31-(8/3)=-5/3. Por tanto, una llamada a la función debe devolver aproximadamente (0,26,1.6667)(0,26,-1.6667). Esta cuenta sencilla conecta la ecuación, el orden de las coordenadas y el arreglo producido por el programa. Si apareciera (26,0,1.6667)(26,0,-1.6667), el problema no sería una tolerancia insuficiente, sino un intercambio de componentes.

La primera ejecución útil no tiene que producir todavía el atractor. Puede integrar de t=0t=0 a t=0.1t=0.1, guardar once muestras y comprobar que todas son finitas. Ese intervalo corto facilita inspeccionar los datos y descubrir una escala temporal equivocada. Después se amplía el horizonte a 8080, se descartan las primeras 2020 unidades como transitorio y se conserva tanto la trayectoria completa como la máscara usada para seleccionar la parte mostrada. Guardar solo los puntos posteriores al transitorio impediría revisar más tarde si el corte fue razonable.

Supongamos ahora que se quiere estudiar sensibilidad. La configuración baseline.json usa x0=1x_0=1 y una segunda configuración usa x0=1+106x_0=1+10^{-6}; todo lo demás permanece idéntico. Copiar y modificar a mano dos archivos completos es arriesgado porque una tolerancia puede cambiar sin querer. Es preferible cargar la base, modificar únicamente la coordenada y registrar la diferencia en el manifiesto. El resultado no se resume con “las curvas se separan”: se calcula d(t)=ua(t)ub(t)2d(t)=\lVert u_a(t)-u_b(t)\rVert_2 sobre la misma malla temporal. Si los integradores devuelven tiempos distintos, primero se solicita una malla común o se interpola con una regla declarada; restar filas por posición no tiene significado garantizado.

Esta comparación también muestra por qué reproducibilidad no equivale a obtener bytes idénticos en cualquier máquina. Diferencias de biblioteca, arquitectura o paralelismo pueden cambiar los últimos bits y, en un sistema sensible, amplificarse con el tiempo. Lo exigible es conservar las condiciones del cálculo, verificar invariantes y tolerancias, y explicar el margen de comparación. Para una prueba corta se puede exigir coincidencia numérica estrecha; para una trayectoria caótica larga se comparan estadísticas, secciones o distribuciones, no cada punto final.

El identificador de la corrida debe depender de la configuración canónica, no del orden casual de las claves del JSON. Serializar con claves ordenadas y separadores fijos antes de calcular el hash hace que dos configuraciones equivalentes reciban el mismo identificador. El manifiesto conserva además el archivo original: el hash localiza, mientras el texto permite leer qué se hizo.

Trazabilidad de la figura

Atractor de Lorenz con ejes identificados
Para reconstruir esta figura hacen falta campo vectorial, parámetros, estado inicial, integración, transitorio, proyección y estilo. El PNG por sí solo no conserva esa cadena.

Atlas reproducible de sistemas dinámicos Función: plot_lorenz

Descargar .py

El manifiesto acompaña la imagen y registra, como mínimo:

  • configuración completa;
  • versión de Python y ruta del intérprete;
  • versiones de NumPy, SciPy y Matplotlib;
  • fecha en formato ISO y zona horaria;
  • identificador del código, si existe;
  • nombres y tamaños de los archivos generados.

La fecha ayuda a localizar una ejecución, pero no la identifica científicamente. Dos corridas del mismo día pueden usar parámetros distintos. Un nombre legible y un pequeño hash de la configuración evitan colisiones.

Pruebas unitarias previas a la integración

Una prueba del campo usa un estado cuyo resultado pueda calcularse a mano:

import numpy as np

def test_lorenz_at_origin() -> None:
    value = lorenz(0.0, np.zeros(3), LorenzParameters())
    np.testing.assert_allclose(value, np.zeros(3))

def test_lorenz_rejects_wrong_shape() -> None:
    try:
        lorenz(0.0, np.zeros((3, 1)), LorenzParameters())
    except ValueError as error:
        assert "shape" in str(error)
    else:
        raise AssertionError("La forma incorrecta no fue rechazada")

Estas pruebas no demuestran que toda la simulación sea correcta. Detectan contratos rotos y valores conocidos. Después se añaden una solución exacta para calibrar el integrador y una prueba que compruebe columnas, finitud y longitud de los datos guardados.

Práctica ejecutable

Funciones y decisiones sobre una lista

Pyodide v314.0.4

Ejecuta el programa, comprueba el promedio a mano y modifica la lista. Después cambia la condición para contar valores menores que el promedio.

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 20 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.

Auditoría mínima del proyecto de Lorenz

  1. Convierte los parámetros de un oscilador en una clase inmutable y rechaza valores físicamente imposibles.
  2. Escribe una prueba que falle si el campo devuelve una forma distinta de la del estado.
  3. Carga y valida una configuración JSON sin corregir entradas silenciosamente.
  4. Registra versiones y ruta del intérprete en un manifiesto.
  5. Reconstruye la carpeta desde cero siguiendo únicamente el README.

Una persona que reciba únicamente el repositorio debe poder producir la corrida base y reconocer, desde el manifiesto, cualquier diferencia de entorno. Ese es el ensayo práctico de la organización propuesta.

Fuentes consultadas

Obras citadas en el desarrollo; los localizadores indican los capítulos o secciones consultados.
  1. Python Software Foundation (2026). The Python Tutorial. Documentación de Python 3 Python Software Foundation.

    §§3–8: introducción informal, control de flujo, estructuras de datos, módulos, entrada/salida y errores · Base del lenguaje usada para variables, tipos, colecciones, decisiones, ciclos, funciones, módulos, excepciones y archivos.
  2. Edward N. Lorenz (1993). The Essence of Chaos. University of Washington Press.

    caps. 1–3 · Procedencia científica del experimento de sensibilidad usado en la unidad.
  3. Kathleen T. Alligood, Tim D. Sauer y James A. Yorke (1996). Chaos: An Introduction to Dynamical Systems. Springer. ISBN 0-387-94677-2.

    visitas de laboratorio y apéndice B · Experimentos computacionales y controles numéricos adaptados.