Comida clave
- Depuración El código científico de Python sigue una escala de escalamiento:
print,assert,pdb, perfiles y pruebas. - La mayoría de los errores científicos son numéricos, como los valores de NAN, las discrepancias en la forma de los arreglos, la pérdida de precisión o los pasos de tiempo inestables.
print()Atrapa muchos errores para principiantes, pero las afirmaciones y los depuradores facilitan el aislamiento de las fallas.- Un flujo de trabajo de depuración científica confiable es: hacer que falle, aísle el problema, cambie una cosa, verifique y agregue una prueba.
- La creación de perfiles puede revelar errores ocultos porque el código lento es a menudo un signo de lógica de matriz incorrecta, bucles accidentales o una asignación excesiva.
Depurar el código científico Python es una de las habilidades más frustrantes que se pueden desarrollar. Escribe lo que debería ser una simple simulación de difusión de calor, el código se ejecuta sin errores y la salida es completamente incorrecta.
Sin choque. Sin rastreo. solo números incorrectos en silencio.
Ese es el desafío único de la depuración científica. Los insectos más peligrosos son a menudo los que no se estrellan en absoluto. Un número incorrecto puede propagarse a través de miles de iteraciones y producir un resultado que parece físicamente plausible hasta que verifique la conservación, la estabilidad o la convergencia.
Esta guía explica cómo depurar el código científico de Python en varios niveles: declaraciones de impresión, registro, aserciones, depuración interactiva, perfiles y pruebas de regresión.
Ya sea que esté ejecutando una primera simulación FIPY o depurando una canalización de investigación de producción, este flujo de trabajo le brinda una estructura práctica a seguir.
¿Qué tiene de diferente la depuración del código científico?
La depuración científica es diferente de la depuración de una aplicación web, API o canalización de datos ordinaria.
Los números pueden mentir. Una aplicación web a menudo falla visiblemente: un botón no funciona, una API devuelve un error o falla una consulta de base de datos. El código científico puede funcionar perfectamente y aún así producir resultados incorrectos. Una concentración negativa, un campo de energía explosiva o una NAN oculta solo puede aparecer si lo comprueba directamente.
La corrección se define por la física, no solo por la sintaxis. Un programa de Python que se ejecuta es sintácticamente válido. Un programa científico solo es creíble si coincide con las soluciones conocidas, conserva las cantidades esperadas, respeta las condiciones de los límites y converge bajo el refinamiento.
Los rastros de pila a menudo apuntan a números internos numpy, scipy o fip. El error real puede estar en su configuración, pero el mensaje de error puede aparecer en lo más profundo de una biblioteca. Es por eso que la depuración científica necesita un proceso estructurado, no solo la lectura de rastreo.
El flujo de trabajo de depuración: un modelo mental de cinco pasos
Un útil flujo de trabajo de depuración para código científico sigue cinco pasos.
Paso 1: Haz que falle
Su primer trabajo es crear un caso de prueba que falla cada vez. En el código científico, esto generalmente significa construir un ejemplo reproducible mínimo que produce un resultado incorrecto.
No necesita la simulación completa. Necesita el código más pequeño que demuestre el error.
import numpy as np
# Full simulation may be hundreds of lines.
# Start with the smallest failing update step.
phi = np.ones((10, 10))
phi[:] = 0.5
gradient = np.gradient(phi)[0]
phi_new = phi - 0.1 * gradient
print(f"min(phi_new) = {np.min(phi_new)}")
print(f"max(phi_new) = {np.max(phi_new)}")
Al aislar el paso de actualización, crea un caso reproducible. Esta es la base de cada paso de depuración posterior.
Paso 2: Dividir y conquistar
Una vez que tengas un caso fallido, aíslalo más. Encuentre el módulo, función, línea o término de física responsable de la falla.
Para el código de simulación, una táctica útil es probar cada término de física por separado. Si su modelo combina difusión, reacción y advección, pruebe cada uno solo antes de depurar el sistema acoplado.
solution_diffusion = solve_diffusion_only(initial_conditions, dt)
assert np.all(solution_diffusion >= 0), "Diffusion produced negative values"
solution_reaction = solve_reaction_only(initial_conditions, dt)
assert np.all(solution_reaction >= 0), "Reaction produced negative values"
solution_coupled = solve_coupled(initial_conditions, dt)
Esto reduce el espacio de búsqueda. En lugar de depurar toda la simulación, depura un término o un mecanismo de acoplamiento.
Paso 3: Cambiar una cosa a la vez
La depuración científica se vuelve confusa cuando cambias varias variables a la vez. Si cambia el paso de tiempo, las condiciones de los límites, la tolerancia al solucionador y la condición inicial juntos, no puede saber qué cambio importaba.
Cambiar un parámetro. correr comparar documento.
Utilice el control de versiones para realizar un seguimiento de los cambios. Incluso las confirmaciones simples como «DT cambiadas de 0.01 a 0.001» pueden ahorrar horas después.
Paso 4: Usa el depurador
Cuando las impresiones y las aserciones no sean suficientes, use pdb, ipdb o un depurador IDE. Un depurador le permite pausar la ejecución, inspeccionar variables, pasar a través del código y verificar por qué cambió un valor.
Paso 5: agregar a la suite de prueba
Después de corregir un error, agregue una prueba que lo habría detectado. Esto evita que el mismo error regrese más tarde.
def test_no_negative_phases():
"""Verify phase field stays non-negative after diffusion."""
phi = np.ones((10, 10))
phi = diffusion_step(phi, dt=0.01)
assert np.min(phi) >= 0, f"Got min value {np.min(phi)}"
El flujo de trabajo es simple: aislar, dividir, cambiar una cosa, depurar y probar. El resto de esta guía explica qué herramientas usar en cada nivel.
Nivel 1: Imprimir declaraciones y registro
print() sigue siendo útil. Es la herramienta de depuración más simple y atrapa muchos errores tempranos. La clave es usarlo con estructura.
El problema con las malas impresiones
Un error principiante común es imprimir valores sin contexto.
for i in range(100):
print(phi[i])
Después de algunas iteraciones, tiene muchos números sin un significado claro. No sabe qué variable, paso de tiempo o condición produjo cada línea.
La forma correcta: impresiones estructuradas
Etiqueta todo. Incluya el paso de tiempo, el nombre de la variable, el mínimo, el máximo y cualquier cantidad conservada que le interese.
import numpy as np
print(
f"[t={t:06.4f}] "
f"min(phi)={np.min(phi):.6f}, "
f"max(phi)={np.max(phi):.6f}"
)
print(
f"[t={t:06.4f}] "
f"energy={np.sum(phi * phi):.6f}, "
f"mass={np.sum(phi):.6f}"
)
Esta salida es fácil de escanear:
[t=0.0010] min(phi)=0.000000, max(phi)=1.000000
[t=0.0010] energy=0.850000, mass=1.000000
[t=0.0020] min(phi)=0.000000, max(phi)=1.023456
[t=0.0020] energy=0.873456, mass=1.012345
Puede ver inmediatamente que el valor máximo se elevó por encima de 1.0 y la masa cambió. Esas son pistas útiles.
Registrar vs declaraciones de impresión
Para una depuración seria, use el módulo logging de Python en lugar de RAW print().
import logging
logging.basicConfig(
level=logging.DEBUG,
format="%(levelname)s: %(message)s"
)
logger = logging.getLogger(__name__)
logger.debug("Running diffusion step at t=%f, dt=%f", t, dt)
logger.warning("Energy increased by %.5f", energy_change)
El registro tiene varias ventajas:
- niveles como
DEBUG,WARNINGyERROR. - Marcas de tiempo opcionales y salida de archivo.
- Fácil activación o desactivación sin eliminar el código de depuración.
- Diagnósticos más limpios durante largos recorridos de simulación.
Una regla práctica es usar print() para scripts exploratorios rápidos y logging para código de simulación reutilizable o de producción.
Nivel 2: Aserciones y controles de cordura
Las afirmaciones son como declaraciones impresas que se niegan a permanecer en silencio. Producen un error cuando se viola una suposición.
El patrón: suposiciones primero
El patrón más útil es verificar las entradas, ejecutar el solucionador y verificar las salidas.
import numpy as np
def solve_diffusion(phi, dt, diffusion_coeff):
# Preconditions
assert np.all(phi >= 0), f"Negative phase: {np.min(phi)}"
assert np.all(phi <= 1), f"Phase > 1: {np.max(phi)}"
assert np.isfinite(phi).all(), "Non-finite input values detected"
# Solver step
phi_new = apply_diffusion(phi, dt, diffusion_coeff)
# Postconditions
assert np.isfinite(phi_new).all(), "Solver produced NaN or Inf"
assert np.all(phi_new >= 0), f"Negative output: {np.min(phi_new)}"
return phi_new
Esto convierte la falla numérica silenciosa en un error claro con la ubicación y el contexto.
La filosofía de la afirmación
Las afirmaciones no se tratan solo de atrapar errores. Fijan errores.
Una afirmación que falla cerca del comienzo de un solucionador le dice dónde se rompió la suposición. Una nana que aparece cientos de líneas más tarde te dice mucho menos.
Los lugares útiles para las afirmaciones incluyen:
- Llamadas antes y después del solucionador.
- en los límites del módulo donde se intercambian los datos.
- Después de operaciones de malla como refinamiento, reajuste o engrosamiento.
- después de aplicar las condiciones de contorno.
Una advertencia sobre las afirmaciones
No use las afirmaciones en exceso en bucles internos caros a menos que las necesite. Pueden agregar sobrecarga de tiempo de ejecución y hacer que la depuración sea ruidosa.
Use aserciones para invariantes y condiciones previas: condiciones que nunca deben violarse si el código es correcto.
Nivel 3: El depurador de Python
Cuando las declaraciones de impresión no sean suficientes, use un depurador interactivo. Un depurador le permite pausar el programa, inspeccionar el estado y pasar por la lógica.
La herramienta clásica: PDB
pdb es el depurador incorporado de Python. Está disponible en cada instalación de Python.
En ipython, puede usar la depuración post-mortem después de un error:
In [1]: %run simulation.py
In [2]: %debug
Esto lo deja caer en el depurador en el punto de falla.
Para los scripts de línea de comandos, inicie el script con el depurador:
python -m pdb simulation.py
Los comandos útiles incluyen:
c: Continúe la ejecución.n: Mover a la siguiente línea.s: Paso a una llamada de función.b: Establezca un punto de interrupción.p variable_name: imprime una variable.q: salga del depurador.
También puede insertar un punto de interrupción directamente en el código:
def solve_phase_field(phi, dt):
import pdb
pdb.set_trace()
phi_new = update_phase_field(phi, dt)
return phi_new
Depuración de IPDB e IPython
ipdb es una versión mejorada de pdb. Proporciona la finalización de la pestaña, un mejor formato y una experiencia interactiva más cómoda.
pip install ipdb
import ipdb
ipdb.set_trace()
Para los usuarios de IPython, %debug suele ser la opción más rápida después de una excepción.
Depuradores IDE
PyCharm, VS Code y Spyder incluyen depuradores gráficos. Le permiten establecer puntos de interrupción, inspeccionar variables, pasar por el código y ver expresiones sin escribir comandos.
Los depuradores IDE son útiles para el desarrollo local. Para clústeres remotos o servidores sin cabeza, pdb, ipdb y el registro suelen ser más prácticos.
Cuándo usar qué herramienta
| Guión | herramienta recomendada |
|---|---|
| guión rápido o desarrollo local temprano | print() Más afirmaciones |
| Trazamiento de error en ipython | %debug |
| Clúster remoto sin acceso IDE | python -m pdb script.py |
| Depuración interactiva con mejor contexto | ipdb o depurador IDE |
| Diagnósticos estilo producción | logging con puntos de interrupción estratégicas |
Nivel 4: Perfiles para encontrar errores ocultos
El perfilado generalmente se ve como una herramienta de rendimiento, pero también ayuda a la depuración. El código lento suele ser un código incorrecto. Los perfiladores pueden revelar bucles de Python accidental, asignaciones repetidas, cálculos redundantes o llamadas a funciones inesperadas.
Cprofile: encontrar las funciones calientes
import cProfile
import pstats
profiler = cProfile.Profile()
profiler.enable()
solve_phase_field(phi, dt, n_iterations)
profiler.disable()
stats = pstats.Stats(profiler)
stats.sort_stats("cumulative")
stats.print_stats(20)
Esto muestra qué funciones consumen más tiempo total. Si una función auxiliar domina inesperadamente el tiempo de ejecución, puede contener un error oculto o una lógica de matriz ineficiente.
line_profiler: encuentra las líneas calientes
Cuando cProfile identifica una función lenta, line_profiler puede mostrar qué líneas dentro de la función consumen tiempo.
pip install line_profiler
kernprof -l -v simulation.py
Esto es útil cuando una sola línea dentro de un bucle crea matrices repetidamente o realiza una operación lenta por error.
Perfiles de memoria: encontrar asignaciones ocultas
El código científico a menudo oculta errores de memoria, como copias accidentales o la creación excesiva de matrices dentro de bucles. memory_profiler puede ayudar a realizar un seguimiento de las asignaciones.
from memory_profiler import profile
@profile
def solve_phase_field(phi, dt):
phi_new = apply_diffusion(phi, dt)
return phi_new
Los perfiles de memoria línea por línea pueden revelar dónde se crean y descartan grandes matrices.
Perfilar como depuración
El perfilado ayuda cuando un solucionador es a la vez incorrecto y lento. Por ejemplo, un corte equivocado puede calcular la energía sobre la región equivocada. Un perfil puede mostrar que el cálculo de la energía es inesperadamente caro, lo que le indica la operación de la matriz.
Las afirmaciones confirman el error. El perfilado le ayuda a encontrar dónde buscar.
Nivel 5: Pruebas y prevención de regresión
El nivel final de depuración es la prevención. Después de corregir un error, escriba una prueba que reproduzca la falla y demuestre que la solución funciona.
El patrón de pruebas científicas
import pytest
import numpy as np
@pytest.mark.parametrize("mesh_size", [10, 20, 50])
def test_energy_decreases(mesh_size):
"""Energy should decrease during simple diffusion."""
phi = init_phi(mesh_size)
energy_history = []
for step in range(100):
energy_history.append(compute_energy(phi))
phi = solve_step(phi, dt=0.01)
energy_changes = np.diff(energy_history)
assert np.all(energy_changes <= 1e-10)
Esta prueba verifica el comportamiento físico, no solo la sintaxis. Una simulación correcta no debe ejecutarse simplemente. Debe conservar las cantidades esperadas, respetar los límites y converger con el refinamiento.
La pirámide de prueba del código científico
- Las pruebas unitarias son rápidas y verifican funciones individuales como
compute_gradient()oapply_boundary_conditions(). - Las pruebas de integración ejecutan comprobaciones a nivel de solucionador contra soluciones analíticas o fabricadas.
- Las pruebas de regresión comparan las simulaciones completas con los resultados de línea de base de confianza.
El principio clave es simple: probar la física, no solo la ruta del código.
Trampas de depuración científica comunes
Nan e infinito
Los valores de NaN e Infinity son comunes en el código científico. A menudo aparecen después de la división por valores cercanos a cero, log(0), sqrt de valores negativos o desbordamiento en exponenciales.
phi = np.zeros((100, 100))
phi[50, 50] = 1.0
result = phi / (phi + 1e-300)
Compruebe los valores finitos después de las operaciones numéricas:
assert np.isfinite(result).all(), (
f"NaN or Inf detected: min={np.nanmin(result)}, max={np.nanmax(result)}"
)
Discrepancias de forma de matriz
Los errores de forma de matriz son comunes cuando se mueven entre matrices de solucionador aplanados y campos 2D o 3D.
print(f"phi shape: {phi.shape}")
print(f"expected shape: {expected_shape}")
Las causas comunes incluyen aplanamiento accidental, indexación que elimina una dimensión y variables de biblioteca que almacenan datos en una forma diferente a la esperada.
Precisión de punto flotante
No compare las matrices de coma flotante con la igualdad exacta a menos que se espere verdaderamente la igualdad exacta. Utilice comparaciones basadas en la tolerancia.
np.testing.assert_allclose(phi, phi_new, rtol=1e-6, atol=1e-12)
Esto es especialmente importante para los solucionadores iterativos, la precisión mixta y las integraciones a largo plazo.
Errores de la condición de límite
Los errores de las condiciones de contorno son comunes porque el código a menudo se ejecuta incluso cuando las celdas o las caras incorrectas están restringidas.
# Wrong: overwrites all cells
phi[:] = boundary_value
# Better: apply only to selected boundary locations
phi[boundary_mask] = boundary_value
Siempre verifique que los valores de límite se apliquen solo donde se pretenda y que los valores interiores no se sobrescriban.
Convergencia y estabilidad
Las simulaciones pueden divergir silenciosamente cuando los pasos de tiempo son demasiado grandes o las tolerancias del solucionador son demasiado bajas. monitorear las cantidades conservadas o acotadas a lo largo del tiempo.
if energy > prev_energy * 1.1:
print(f"Energy growing. Reducing dt from {dt} to {dt / 2}")
dt /= 2
Realice un seguimiento de la energía, la masa, el impulso, los límites, los residuos y el comportamiento de paso de tiempo. Trazarlos durante la depuración.
La escalera de escalada de depuración
Utilice el nivel de depuración más bajo que proporcione suficiente información.
Level 1: print() — quick inspection
Level 2: assert() — sanity checks and invariants
Level 3: pdb/ipdb — interactive state inspection
Level 4: profiling — performance bugs and hidden inefficiency
Level 5: pytest — regression prevention
Escalar cuando sea necesario:
- Si
print()da suficiente contexto, permanezca en el nivel 1. - Si necesita verificar invariantes, use aserciones.
- Si necesita inspeccionar el estado en una línea específica, use
pdboipdb. - Si el código es incorrecto y lento, perfilalo.
- Si desea evitar que el error regrese, escriba una prueba.
Un ejemplo práctico de depuración: arreglar una simulación Fipy
Supongamos que una simulación de difusión físimo produce valores de concentración ilimitados en lugar de difusión suave. Comience por escribir una versión mínima y clara del problema.
import numpy as np
from fipy import Grid2D, CellVariable, TransientTerm, DiffusionTerm
mesh = Grid2D(nx=50, ny=50, dx=1.0, dy=1.0)
phi = CellVariable(name="concentration", mesh=mesh, value=0.0)
x, y = mesh.cellCenters
# Initial hot spot in the center
phi.setValue(
1.0,
where=((x - 25.0)**2 + (y - 25.0)**2) < 25.0
)
eq = TransientTerm(var=phi) == DiffusionTerm(coeff=1.0, var=phi)
dt = 0.01
print(f"Initial: min={phi.value.min()}, max={phi.value.max()}")
for step in range(100):
eq.solve(var=phi, dt=dt)
assert np.isfinite(phi.value).all(), "NaN or Inf detected"
assert phi.value.min() >= -1e-10, f"Negative value at step {step}"
print(f"Final: min={phi.value.min()}, max={phi.value.max()}")
luego depurar sistemáticamente.
Paso 1: Haz que falle
Log Mínimo, Máximo, Masa y Energía antes y después de la carrera.
print(f"min={phi.value.min()}, max={phi.value.max()}")
print(f"mass={np.sum(phi.value * mesh.cellVolumes)}")
print(f"energy={np.sum(phi.value**2 * mesh.cellVolumes)}")
Paso 2: Dividir y conquistar
Pruebe solo el término de difusión. Elimine los términos de reacción, los términos de advección, las fuentes no lineales y las condiciones de contorno complejas hasta que la falla desaparezca o se aísle.
Paso 3: Cambiar una cosa
Pruebe un paso de tiempo más pequeño, luego compare los resultados.
dt = 0.001
Si el error persiste con un paso de tiempo más pequeño, el problema puede ser espacial, estructural o relacionado con las condiciones de los límites en lugar de la estabilidad del tiempo.
Paso 4: Usa el depurador
Caída en un depurador cerca del paso defectuoso e inspeccione los valores, la forma de la malla, las máscaras y las condiciones de los límites.
import pdb
pdb.set_trace()
Paso 5: agregar una prueba
Después de corregir el error, agregue una prueba de regresión.
def test_diffusion_mass_stays_finite():
phi_initial, phi_final, volumes = run_small_diffusion_case()
mass_initial = np.sum(phi_initial * volumes)
mass_final = np.sum(phi_final * volumes)
np.testing.assert_allclose(mass_initial, mass_final, rtol=1e-6)
assert np.isfinite(phi_final).all()
Resumen y próximos pasos
Depurar el código científico Python es una habilidad progresiva. Comience con declaraciones impresas, pase a las aserciones, use depuradores cuando se necesite una inspección de estado, perfile cuando el código sea lento o sospechoso, y escriba pruebas para evitar regresiones.
El principio más importante es aislar el error antes de arreglarlo. Un error que puede reproducir en un pequeño caso de prueba es mucho más fácil de resolver. Un error que solo aparece dentro de una gran simulación sigue siendo un misterio.
Para el código científico, recuerde estas reglas:
- Compruebe si hay NAN e infinito después de operaciones numéricas.
- Imprimir formas de matriz cuando la indexación se vuelve compleja.
- Supervise las cantidades conservadas como la masa, la energía y el momento.
- Pruebe las condiciones de los límites por separado de la lógica interior.
- Use afirmaciones como primera línea de defensa contra fallas numéricas silenciosas.
Próximos pasos
- Comience a usar la escalera de escalada de depuración: Comience con
print()y escale solo cuando sea necesario. - Agrega aserciones antes y después de las funciones del solucionador.
- Configure el registro para los diagnósticos de simulación.
- Escriba al menos un caso de prueba que reproduzca un error común.
La depuración no es un signo de debilidad. Es parte de escribir código que importa. Las habilidades que desarrolle al depurar el código científico Python ayudarán en todo tipo de proyectos de software numérico y de investigación.
Guías relacionadas
- performance perfilando y optimización para los solucionadores de PDE de Python: una guía práctica — herramientas de perfilado, NUMBA y optimización de memoria.
- pruebas unitarias para código científico: estrategias pytest — precisión numérica y patrones de prueba.
- Integración continua para software de investigación: pruebas y validación automatizadas — CI canalizaciones y pruebas automatizadas.
- Mejores prácticas para mantener el código científico — estructura del proyecto, control de versiones y documentación.
- usando fipy para el modelado de campo de fase — patrones de implementación fipy.
¿Necesita ayuda para depurar su código de simulación?
Depuración de simulaciones científicas complejas puede llevar mucho tiempo, especialmente cuando no está seguro de si el error está en la configuración de código, malla, condiciones de contorno o del solucionador.
Nuestro equipo puede ayudar con los flujos de trabajo de depuración sistemáticos, los casos de pruebas numéricas, las verificaciones de cordura para las leyes de conservación y las canalizaciones de CI con pruebas de regresión.