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 y los parámetros clásicos, la primera componente del campo es ; la segunda es ; la tercera es . Por tanto, una llamada a la función debe devolver aproximadamente . Esta cuenta sencilla conecta la ecuación, el orden de las coordenadas y el arreglo producido por el programa. Si apareciera , 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 a , 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 , se descartan las primeras 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 y una segunda configuración usa
; 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
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
Atlas reproducible de sistemas dinámicos Función: plot_lorenz
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 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
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
- Convierte los parámetros de un oscilador en una clase inmutable y rechaza valores físicamente imposibles.
- Escribe una prueba que falle si el campo devuelve una forma distinta de la del estado.
- Carga y valida una configuración JSON sin corregir entradas silenciosamente.
- Registra versiones y ruta del intérprete en un manifiesto.
- 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.- §§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.
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.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.