Reading Time: 12 minutes

Points à retenir clés

  • Le débogage du code Python scientifique suit une échelle d’escalade : print, assert, pdb, profilage et tests.
  • La plupart des bogues scientifiques sont numériques, tels que les valeurs de NAN, les incompatibilités de forme de tableau, la perte de précision ou les pas de temps instables.
  • print() attrape de nombreux bogues pour débutants, mais les assertions et les débogueurs facilitent l’isolement des échecs.
  • Un workflow de débogage scientifique fiable est : faites-le échouer de manière fiable, isolez le problème, modifiez une chose, vérifiez et ajoutez un test.
  • Le profilage peut révéler des bogues cachés car le code lent est souvent le signe d’une logique de tableau, de boucles accidentelles ou d’une allocation excessive.

Le débogage du code Python scientifique est l’une des compétences les plus frustrantes à développer. Vous écrivez ce qui devrait être une simple simulation de diffusion de la chaleur, le code s’exécute sans erreur et la sortie est complètement erronée.

Pas de crash. Pas de trace. Des chiffres juste en silence.

C’est le défi unique du débogage scientifique. Les insectes les plus dangereux sont souvent ceux qui ne plantent pas du tout. Un nombre incorrect peut se propager à travers des milliers d’itérations et produire un résultat qui semble physiquement plausible jusqu’à ce que vous vérifiiez la conservation, la stabilité ou la convergence.

Ce guide explique comment déboguer le code Python scientifique à plusieurs niveaux : déclarations d’impression, journalisation, assertions, débogage interactif, profilage et régression.

Que vous exécutiez une première simulation FIPY ou déboguez un pipeline de recherche de production, ce flux de travail vous donne une structure pratique à suivre.

Qu’y a-t-il de différent dans le débogage du code scientifique ?

Le débogage scientifique est différent du débogage d’une application Web, d’une API ou d’un pipeline de données ordinaire.

Les chiffres peuvent mentir. Une application Web échoue souvent de manière visible : un bouton ne fonctionne pas, une API renvoie une erreur ou une requête de base de données échoue. Le code scientifique peut fonctionner parfaitement et produire des résultats erronés. Une concentration négative, un champ d’énergie explosif ou une NAN cachée ne peut apparaître que si vous le vérifiez directement.

L’exactitude est définie par la physique, pas seulement par la syntaxe. Un programme Python qui s’exécute est syntaxiquement valide. Un programme scientifique n’est crédible que s’il correspond à des solutions connues, conserve des quantités attendues, respecte les conditions aux limites et converge vers le raffinement.

Les traces d’empilage pointent souvent vers des internes NumPy, Scipy ou Fipy. Le vrai bogue peut être dans votre configuration, mais le message d’erreur peut apparaître au plus profond d’une bibliothèque. C’est pourquoi le débogage scientifique a besoin d’un processus structuré, pas seulement de la lecture des traces.

Le flux de travail de débogage : un modèle mental en cinq étapes

Un flux de travail de débogage utile pour le code scientifique suit cinq étapes.

Étape 1 : faites-le échouer de manière fiable

Votre premier travail consiste à créer un cas de test qui échoue à chaque fois. Dans le code scientifique, cela signifie généralement construire un exemple reproductible minimal qui produit le mauvais résultat.

Vous n’avez pas besoin de la simulation complète. Vous avez besoin du plus petit morceau de code qui démontre le bogue.

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)}")

En isolant l’étape de mise à jour, vous créez un cas reproductible. C’est la base de chaque étape de débogage ultérieure.

Étape 2 : Diviser et conquérir

Une fois que vous avez un cas défaillant, isolez-le davantage. Trouvez le module, la fonction, la ligne ou le terme physique responsable de la défaillance.

Pour le code de simulation, une tactique utile consiste à tester séparément chaque terme de physique. Si votre modèle combine la diffusion, la réaction et l’advection, testez chacun seul avant de déboguer le système couplé.

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)

Cela réduit l’espace de recherche. Au lieu de déboguer l’intégralité de la simulation, vous déboguez un terme ou un mécanisme de couplage.

Étape 3 : Changez une chose à la fois

Le débogage scientifique devient déroutant lorsque vous modifiez plusieurs variables à la fois. Si vous modifiez ensemble le pas de temps, les conditions aux limites, la tolérance du solveur et la condition initiale, vous ne pouvez pas savoir quel changement importait.

Modifier un paramètre. Courez. Comparez. Document.

Utilisez le contrôle de version pour suivre les modifications. Même de simples commits tels que « DT changé de 0,01 à 0,001 » peuvent économiser des heures plus tard.

Étape 4 : Utilisez le débogueur

Lorsque les impressions et les assertions ne suffisent pas, utilisez pdb, ipdb ou un débogueur IDE. Un débogueur vous permet de suspendre l’exécution, d’inspecter les variables, de parcourir le code et de vérifier pourquoi une valeur a changé.

Étape 5 : Ajouter à la suite de tests

Après avoir corrigé un bogue, ajoutez un test qui l’aurait attrapé. Cela empêche le même bogue de revenir plus tard.

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)}"

Le flux de travail est simple : isoler, diviser, changer une chose, déboguer et tester. Le reste de ce guide explique les outils à utiliser à chaque niveau.

Niveau 1 : instructions d’impression et journalisation

print() est toujours utile. Il s’agit de l’outil de débogage le plus simple et attrape de nombreux premiers bogues. La clé est de l’utiliser avec la structure.

Le problème avec les mauvaises impressions

Une erreur courante de débutant est l’impression de valeurs sans contexte.

for i in range(100):
    print(phi[i])

Après quelques itérations, vous avez de nombreux chiffres sans aucune signification claire. Vous ne savez pas quelle variable, pas de temps ou condition produit chaque ligne.

La bonne manière : des impressions structurées

Étiquetez tout. Incluez le pas de temps, le nom de la variable, le minimum, le maximum et toute quantité conservée qui vous intéresse.

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}"
)

Cette sortie est facile à numériser :

[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

Vous pouvez immédiatement voir que la valeur maximale a augmenté au-dessus de 1,0 et que la masse a changé. Ce sont des indices utiles.

Enregistrement vs instructions d’impression

Pour le débogage sérieux, utilisez le module logging de Python au lieu 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)

La journalisation présente plusieurs avantages :

  • niveaux tels que DEBUG, WARNING et ERROR.
  • Horodatages et sortie de fichiers en option.
  • Activation ou désactivation facile sans suppression du code de débogage.
  • Des diagnostics plus nets lors de longues séries de simulation.

Une règle pratique consiste à utiliser print() pour les scripts exploratoires rapides et logging pour le code de simulation réutilisable ou de type production.

Niveau 2 : Assertions et vérifications de santé mentale

Les assertions sont comme des déclarations d’impression qui refusent de garder le silence. Ils soulèvent une erreur lorsqu’une hypothèse est violée.

Le modèle : les hypothèses d’abord

Le modèle le plus utile consiste à vérifier les entrées, à exécuter le solveur et à vérifier les sorties.

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

Cela transforme une défaillance numérique silencieuse en une erreur claire avec l’emplacement et le contexte.

La philosophie d’assertion

Les assertions ne concernent pas seulement les bugs. Ils épinglent les bugs.

Une assertion qui échoue au début d’un solveur vous indique où l’hypothèse s’est cassée. Une NAN qui apparaît des centaines de lignes plus tard vous en dit beaucoup moins.

Les lieux utiles pour les assertions comprennent :

  • Avant et après les appels du solveur.
  • aux limites des modules où les données sont échangées.
  • Après des opérations de maillage telles que le raffinement, le remaillage ou le grossissement.
  • Après avoir appliqué les conditions aux limites.

Un avertissement sur les assertions

N’utilisez pas trop les assertions dans les boucles intérieures coûteuses, sauf si vous en avez besoin. Ils peuvent ajouter des frais généraux d’exécution et rendre le débogage bruyant.

Utilisez des assertions pour les invariants et les conditions préalables : conditions qui ne doivent jamais être violées si le code est correct.

Niveau 3 : Le débogueur Python

Lorsque les instructions d’impression ne suffisent pas, utilisez un débogueur interactif. Un débogueur vous permet de mettre en pause le programme, d’inspecter l’état et de parcourir la logique.

L’outil classique : PDB

pdb est le débogueur intégré de Python. Il est disponible dans chaque installation Python.

Dans IPython, vous pouvez utiliser le débogage post-mortem après une erreur :

In [1]: %run simulation.py

In [2]: %debug

Cela vous plonge dans le débogueur au point de défaillance.

Pour les scripts de ligne de commande, lancez le script avec le débogueur :

python -m pdb simulation.py

Les commandes utiles incluent :

  • c : Poursuivre l’exécution.
  • n : Passez à la ligne suivante.
  • s : entrez dans un appel de fonction.
  • b : définissez un point d’arrêt.
  • p variable_name : Imprimez une variable.
  • q : Quittez le débogueur.

Vous pouvez également insérer un point d’arrêt directement dans le code :

def solve_phase_field(phi, dt):
    import pdb
    pdb.set_trace()
    
    phi_new = update_phase_field(phi, dt)
    return phi_new

Débogage IPDB et IPython

ipdb est une version améliorée de pdb. Il offre une complétion d’onglets, un meilleur formatage et une expérience interactive plus confortable.

pip install ipdb
import ipdb
ipdb.set_trace()

Pour les utilisateurs d’iPython, %debug est souvent l’option la plus rapide après une exception.

Débogueurs IDE

PyCharm, VS Code et Spyder incluent des débogueurs graphiques. Ils vous permettent de définir des points d’arrêt, d’inspecter les variables, de parcourir le code et de regarder les expressions sans taper de commandes.

Les débogueurs IDE sont utiles pour le développement local. Pour les clusters distants ou les serveurs sans tête, pdb, ipdb et la journalisation sont généralement plus pratiques.

Quand utiliser quel outil

Scénario Outil recommandé
Script rapide ou développement local précoce print() Plus les assertions
Retrait d’erreur dans IPython %debug
Cluster distant sans accès IDE python -m pdb script.py
Débogage interactif avec un meilleur contexte ipdb ou débogueur IDE
Diagnostics de type production logging avec des points d’arrêt stratégiques

Niveau 4 : Profilage pour trouver des bogues cachés

Le profilage est généralement considéré comme un outil de performance, mais il aide également à déboguer. Le code lent est souvent un code erroné. Les profileurs peuvent révéler des boucles Python accidentelles, des allocations répétées, des calculs redondants ou des appels de fonctions inattendus.

cProfile : recherchez les fonctions à chaud

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)

Cela montre les fonctions qui consomment le plus de temps total. Si une fonction d’aide domine de manière inattendue l’exécution, elle peut contenir un bogue caché ou une logique de tableau inefficace.

line_profiler : recherchez les lignes chaudes

Lorsque cProfile identifie une fonction lente, line_profiler peut montrer quelles lignes à l’intérieur de la fonction consomment du temps.

pip install line_profiler
kernprof -l -v simulation.py

Ceci est utile lorsqu’une seule ligne à l’intérieur d’une boucle crée des tableaux à plusieurs reprises ou effectue une opération lente par erreur.

Profilage de la mémoire : rechercher des allocations cachées

Le code scientifique masque souvent les bogues de mémoire tels que les copies accidentelles ou la création de tableaux excessifs à l’intérieur des boucles. memory_profiler peut aider à suivre les allocations.

from memory_profiler import profile

@profile
def solve_phase_field(phi, dt):
    phi_new = apply_diffusion(phi, dt)
    return phi_new

Les profils de mémoire ligne par ligne peuvent révéler où de grands tableaux sont créés et supprimés.

Profilage comme débogage

Le profilage aide lorsqu’un solveur est à la fois faux et lent. Par exemple, une mauvaise tranche peut calculer de l’énergie sur la mauvaise région. Un profil peut montrer que le calcul de l’énergie est étonnamment coûteux, ce qui vous oriente vers le fonctionnement du tableau.

Les assertions confirment le bogue. Le profilage vous aide à trouver où chercher.

Niveau 5 : tests et prévention de la régression

Le niveau final de débogage est la prévention. Après avoir corrigé un bogue, écrivez un test qui reproduit l’échec et prouve que le correctif fonctionne.

Le modèle de test scientifique

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)

Ce test vérifie le comportement physique, pas seulement la syntaxe. Une simulation correcte ne doit pas simplement être exécutée. Il doit conserver les quantités attendues, respecter les limites et converger avec le raffinement.

La pyramide de test pour le code scientifique

  • Les tests unitaires sont rapides et vérifient les fonctions individuelles telles que compute_gradient() ou apply_boundary_conditions().
  • Les tests d’intégration effectuent des vérifications au niveau du solveur par rapport aux solutions analytiques ou fabriquées.
  • Les tests de régression comparent les simulations complètes avec les sorties de référence de confiance.

Le principe clé est simple : tester la physique, pas seulement le chemin du code.

Les pièges courants du débogage scientifique

Nan et l’infini

Les valeurs NAN et Infinity sont courantes dans le code scientifique. Ils apparaissent souvent après une division par des valeurs proches de zéro, log(0), sqrt de valeurs négatives ou débordement d’exponentielles.

phi = np.zeros((100, 100))
phi[50, 50] = 1.0

result = phi / (phi + 1e-300)

Vérifiez les valeurs finies après des opérations numériques :

assert np.isfinite(result).all(), (
    f"NaN or Inf detected: min={np.nanmin(result)}, max={np.nanmax(result)}"
)

Incompatibilités de forme de tableau

Les erreurs de forme de tableau sont courantes lors du passage entre des tableaux de solveurs aplatis et des champs 2D ou 3D.

print(f"phi shape: {phi.shape}")
print(f"expected shape: {expected_shape}")

Les causes courantes comprennent la mise à plat accidentelle, l’indexation qui supprime une dimension et les variables de bibliothèque qui stockent des données dans une forme différente de celle attendue.

Précision à virgule flottante

Ne comparez pas les tableaux à virgule flottante avec une égalité exacte à moins que l’égalité exacte ne soit vraiment attendue. Utilisez des comparaisons basées sur les tolérances.

np.testing.assert_allclose(phi, phi_new, rtol=1e-6, atol=1e-12)

Ceci est particulièrement important pour les solveurs itératifs, la précision mixte et les intégrations à long terme.

Bugs de condition des limites

Les bogues des conditions aux limites sont courants car le code s’exécute souvent même lorsque les mauvaises cellules ou faces sont contraintes.

# Wrong: overwrites all cells
phi[:] = boundary_value

# Better: apply only to selected boundary locations
phi[boundary_mask] = boundary_value

Vérifiez toujours que les valeurs limites ne sont appliquées que lorsque cela est prévu et que les valeurs intérieures ne sont pas écrasées.

Convergence et stabilité

Les simulations peuvent diverger silencieusement lorsque les pas de temps sont trop importants ou que les tolérances des solveurs sont trop lâches. Surveiller les quantités conservées ou délimitées au fil du temps.

if energy > prev_energy * 1.1:
    print(f"Energy growing. Reducing dt from {dt} to {dt / 2}")
    dt /= 2

Suivre l’énergie, la masse, la quantité de mouvement, les limites, les résidus et le comportement de pas de temps. Tracez-les pendant le débogage.

L’échelle de débogage d’escalade

Utilisez le niveau de débogage le plus bas qui donne suffisamment d’informations.

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

Monter en cas de besoin :

  • Si print() donne suffisamment de contexte, restez au niveau 1.
  • Si vous devez vérifier les invariants, utilisez des assertions.
  • Si vous devez inspecter l’état à une ligne spécifique, utilisez pdb ou ipdb.
  • Si le code est erroné et lent, profilez-le.
  • Si vous souhaitez empêcher le retour du bogue, rédigez un test.

Un exemple de débogage pratique : correction d’une simulation Fipy

Supposons qu’une simulation de diffusion FIPY produit des valeurs de concentration non limitatives au lieu d’une diffusion lisse. Commencez par écrire une version minimale et claire du problème.

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()}")

Puis déboguer systématiquement.

Étape 1 : faites-le échouer de manière fiable

Enregistrer le minimum, le maximum, la masse et l’énergie avant et après la course.

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)}")

Étape 2 : Diviser et conquérir

Testez le terme de diffusion seul. Supprimez les termes de réaction, les termes d’advection, les sources non linéaires et les conditions aux limites complexes jusqu’à ce que la défaillance disparaisse ou soit isolée.

Étape 3 : Changez une chose

Essayez un pas de temps plus petit, puis comparez les résultats.

dt = 0.001

Si le bogue persiste avec un pas de temps plus petit, le problème peut être spatial, structurel ou lié aux conditions aux limites plutôt qu’à la stabilité du temps.

Étape 4 : Utilisez le débogueur

Déposez dans un débogueur près de l’étape défaillante et inspectez les valeurs, la forme de maillage, les masques et les conditions aux limites.

import pdb
pdb.set_trace()

Étape 5 : Ajoutez un test

Après avoir corrigé le bogue, ajoutez un test de régression.

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()

Résumé et étapes suivantes

Le débogage du code Python scientifique est une compétence progressive. Commencez par les instructions d’impression, passez aux assertions, utilisez les débogueurs lorsque l’inspection d’état est nécessaire, profilez-le lorsque le code est lent ou suspect, et écrivez des tests pour éviter les régressions.

Le principe le plus important est d’isoler le bogue avant de le corriger. Un bogue que vous pouvez reproduire dans un petit cas de test est beaucoup plus facile à résoudre. Un bug qui n’apparaît qu’à l’intérieur d’une grande simulation reste un mystère.

Pour le code scientifique, n’oubliez pas ces règles :

  • Vérifier la NAN et l’infini après des opérations numériques.
  • Imprimez des formes de tableaux lorsque l’indexation devient complexe.
  • Surveillez les quantités conservées telles que la masse, l’énergie et la quantité de mouvement.
  • Testez les conditions aux limites séparées de la logique intérieure.
  • Utilisez des assertions comme première ligne de défense contre une défaillance numérique silencieuse.

Prochaines étapes

  • Commencez à utiliser l’échelle de débogage d’escalade : commencez par print() et montez uniquement en cas de besoin.
  • Ajoutez des assertions avant et après les fonctions du solveur.
  • Configurez la journalisation pour les diagnostics de simulation.
  • Écrivez au moins un cas de test qui reproduit un bug commun.

Le débogage n’est pas un signe de faiblesse. Cela fait partie de la rédaction de code qui compte. Les compétences que vous développez lors du débogage du code Python scientifique vous aideront dans tous les types de projets de logiciels numériques et de recherche.

Guides connexes

Besoin d’aide pour déboguer votre code de simulation ?

Le débogage de simulations scientifiques complexes peut prendre du temps, surtout lorsque vous ne savez pas si le bogue est dans les paramètres de code, de maillage, de limites ou de solveur.

Notre équipe peut vous aider avec les workflows de débogage systématiques, les cas de test numériques, les contrôles de santé mentale pour les lois de conservation et les pipelines CI avec des tests de régression.