Comment post-traiter des résultats de simulation avec Python et Pandas (Ce que j’aurais fait en 2001 si Python avait existé)


En 2001, je post-traitais mes résultats éléments finis à la main.

SAMCEF sortait ses résultats ; contraintes, déplacements, réactions d’appui. Je les relevais un par un, je les entrais dans Excel, je construisais mes tableaux de marges à la main. Pour les calculs de fatigue, je cherchais les contraintes maximales sur chaque nœud critique, je les injectais dans mes formules de durée de vie, je calculais mes Reserve Factors.

C’était gérable parce qu’on n’avait pas beaucoup de cas de charge. Mais c’était lent, répétitif, et surtout risqué. Une erreur de saisie, une cellule mal copiée et le calcul de durée de vie était faux sans que personne ne s’en aperçoive.

Aujourd’hui, avec Python et Pandas, ce travail se fait en quelques lignes. Pas parce que la mécanique a changé (les formules de fatigue sont les mêmes qu’en 2001), mais parce qu’on peut automatiser tout ce qui était manuel, traiter des centaines de cas de charge en une seconde, et surtout tracer la chaîne depuis les résultats bruts jusqu’au Reserve Factor final, de façon reproductible et vérifiable.

C’est ce que je vous montre dans cet article.


Le problème concret : du logiciel de calcul à la décision

Voici le workflow typique d’un ingénieur structure qui post-traite des résultats éléments finis :

Logiciel de calcul (Samcef, Abaqus, Nastran...)
        ↓
Export des résultats (CSV, TXT, Excel...)
        ↓
Identification des valeurs critiques (σ max, déplacements...)
        ↓
Application des formules métier (fatigue, RF...)
        ↓
Tableau de synthèse → décision

Chaque flèche de ce schéma était manuelle en 2001. Avec Python, toutes ces flèches deviennent du code reproductible, automatisable et vérifiable par quelqu’un d’autre.


Le dataset de l’article

Pour illustrer, on travaille avec un fichier CSV qui simule un export typique depuis un logiciel éléments finis ; les résultats de contraintes sur plusieurs nœuds pour plusieurs cas de charge.

C’est exactement le type de fichier que SAMCEF ou SAFE peut exporter et que vous copiez-colliez dans Excel en 2001.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

# Simulation d'un export éléments finis
# Dans la vraie vie : pd.read_csv('resultats_samcef.csv', sep=';')
np.random.seed(42)

noeuds = [f'N{i:04d}' for i in range(1, 51)]
cas_de_charge = ['LC1_Manoeuvre', 'LC2_Rafale', 'LC3_Atterrissage',
                 'LC4_Pressurisation', 'LC5_Fatigue']

data = []
for noeud in noeuds:
    for cas in cas_de_charge:
        sigma_xx = np.random.uniform(-300, 300)   # MPa
        sigma_yy = np.random.uniform(-200, 200)
        sigma_xy = np.random.uniform(-100, 100)
        deplacement = np.random.uniform(0, 5)     # mm
        data.append([noeud, cas, sigma_xx, sigma_yy, sigma_xy, deplacement])

df = pd.DataFrame(data, columns=[
    'noeud', 'cas_de_charge',
    'sigma_xx_mpa', 'sigma_yy_mpa', 'sigma_xy_mpa',
    'deplacement_mm'
])

print(f"Dataset : {df.shape[0]} lignes × {df.shape[1]} colonnes")
print(f"Nœuds   : {df['noeud'].nunique()}")
print(f"Cas     : {df['cas_de_charge'].nunique()}")
print(df.head())
Dataset : 250 lignes × 6 colonnes
Nœuds   : 50
Cas     : 5
   noeud       cas_de_charge  sigma_xx_mpa  sigma_yy_mpa  sigma_xy_mpa  \
0  N0001       LC1_Manoeuvre    -75.275929    180.285723     46.398788   
1  N0001          LC2_Rafale   -206.388816   -137.602192    -88.383278   
2  N0001    LC3_Atterrissage     60.669007     83.229031    -95.883101   
3  N0001  LC4_Pressurisation    199.465584   -115.064356    -63.635007   
4  N0001         LC5_Fatigue   -117.454654      9.902573    -13.610996   

   deplacement_mm  
0        2.993292  
1        4.330881  
2        4.849549  
3        0.917023  
4        1.456146

Partie 1 — Extraire les contraintes critiques

La première tâche de tout post-traitement : trouver où ça fait le plus mal. Sur quel nœud, sous quel cas de charge la contrainte est-elle maximale ?

En 2001, on parcourait les tableaux de résultats à la main. Avec Pandas, une ligne suffit.

Calcul de la contrainte de Von Mises :

La contrainte de Von Mises est l’indicateur de référence pour évaluer le risque de plastification. Elle combine les composantes du tenseur de contrainte en une valeur scalaire :

# Contrainte de Von Mises (état plan de contraintes)
# σ_VM = √(σxx² - σxx·σyy + σyy² + 3·σxy²)
df['sigma_vm_mpa'] = np.sqrt(
    df['sigma_xx_mpa']**2
    - df['sigma_xx_mpa'] * df['sigma_yy_mpa']
    + df['sigma_yy_mpa']**2
    + 3 * df['sigma_xy_mpa']**2
)

# Top 10 des nœuds les plus chargés (tous cas confondus)
top10 = (df.groupby('noeud')['sigma_vm_mpa']
           .max()
           .sort_values(ascending=False)
           .head(10)
           .reset_index())
top10.columns = ['noeud', 'sigma_vm_max_mpa']

print("Top 10 des nœuds critiques :")
print(top10.to_string(index=False))
Top 10 des nœuds critiques :
noeud  sigma_vm_max_mpa
N0016        426.146630
N0047        419.982170
N0002        416.197348
N0013        405.343009
N0036        402.704097
N0004        402.120678
N0023        387.718441
N0050        380.129585
N0027        377.774206
N0024        377.610313

Identifier le cas de charge le plus sévère par nœud :

# Pour chaque nœud, quel cas de charge donne la contrainte max ?
idx_max = df.groupby('noeud')['sigma_vm_mpa'].idxmax()
noeuds_critiques = df.loc[idx_max, ['noeud', 'cas_de_charge', 'sigma_vm_mpa']]
noeuds_critiques = noeuds_critiques.sort_values('sigma_vm_mpa', ascending=False)

print("Nœud le plus critique :")
print(noeuds_critiques.iloc[0])
Nœud le plus critique :
noeud                         N0016
cas_de_charge    LC4_Pressurisation
sigma_vm_mpa              426.14663

Ce qu’on faisait en regardant les tableaux ligne par ligne pendant 30 minutes se fait en 2 secondes.


Partie 2 — Calcul des Reserve Factors

Le Reserve Factor (RF) est l’indicateur central de la certification structure aéronautique. Il représente le rapport entre la résistance du matériau et la contrainte appliquée — un RF supérieur à 1 signifie que la pièce tient, inférieur à 1 qu’elle cède.

RF = Contrainte admissible / Contrainte appliquée

Sur un projet de certification, on calcule des RF pour chaque nœud critique, chaque cas de charge, chaque mode de défaillance. En 2001, c’était des macros Excel. Avec Python, c’est un pipeline complet.

# Propriétés matériaux (exemple : alliage aluminium 2024-T3)
MATERIAUX = {
    'sigma_rupture_mpa'    : 455,   # Rm
    'sigma_elasticite_mpa' : 325,   # Re0.2
    'sigma_admissible_mpa' : 290,   # Radm (avec coefficient de sécurité)
}

# Calcul du RF pour chaque ligne du dataset
df['RF'] = MATERIAUX['sigma_admissible_mpa'] / df['sigma_vm_mpa']

# Identifier les nœuds hors marge (RF < 1)
hors_marge = df[df['RF'] < 1.0].sort_values('RF')

print(f"Nœuds hors marge (RF < 1) : {len(hors_marge)}")
if len(hors_marge) > 0:
    print(hors_marge[['noeud', 'cas_de_charge', 'sigma_vm_mpa', 'RF']].head(10))

# Tableau de synthèse par cas de charge
synthese_rf = df.groupby('cas_de_charge').agg(
    RF_min=('RF', 'min'),
    RF_moyen=('RF', 'mean'),
    noeuds_hors_marge=('RF', lambda x: (x < 1.0).sum())
).round(3)

print("Synthèse des RF par cas de charge :")
print(synthese_rf.to_string())
Nœuds hors marge (RF < 1) : 45
     noeud       cas_de_charge  sigma_vm_mpa        RF
78   N0016  LC4_Pressurisation    426.146630  0.680517
234  N0047         LC5_Fatigue    419.982170  0.690506
8    N0002  LC4_Pressurisation    416.197348  0.696785
61   N0013          LC2_Rafale    405.343009  0.715443
176  N0036          LC2_Rafale    402.704097  0.720132
17   N0004    LC3_Atterrissage    402.120678  0.721177
110  N0023       LC1_Manoeuvre    387.718441  0.747965
18   N0004  LC4_Pressurisation    382.776233  0.757623
249  N0050         LC5_Fatigue    380.129585  0.762898
132  N0027    LC3_Atterrissage    377.774206  0.767654
Synthèse des RF par cas de charge :
                    RF_min  RF_moyen  noeuds_hors_marge
cas_de_charge                                          
LC1_Manoeuvre        0.748     1.465                  7
LC2_Rafale           0.715     1.421                  9
LC3_Atterrissage     0.721     1.450                  6
LC4_Pressurisation   0.681     1.489                 12
LC5_Fatigue          0.691     1.384                 11

Ce tableau de synthèse qui prenait une demi-journée à construire manuellement sur Excel se génère en quelques millisecondes. Et surtout, il est recalculé automatiquement si les résultats du calcul changent.


Partie 3 — Calcul de durée de vie en fatigue

L’autre grande tâche de mon quotidien d’ingénieur structure : calculer les durées de vie en fatigue à partir des contraintes extraites des calculs éléments finis.

La méthode en industrie aéro ne passe pas par une formule analytique — elle passe par une courbe S-N certifiée fournie par le bureau matériaux. Cette courbe donne, pour un matériau donné, la contrainte admissible en fonction du nombre de cycles. Sur le terrain, on la lit graphiquement (ou on l’interpole) pour déterminer si un nœud est dans les marges.

En pratique, chaque constructeur a ses propres courbes propriétaires, identifiées sur ses propres alliages avec ses propres coefficients de sécurité. Ce que je vous montre ici simule cette démarche avec une table générique.

La logique de vérification :

Objectif de certification : N_certif cycles de vol
Coefficient de sécurité : CS (3 ou 5 selon criticité)
→ On vérifie à N_certif × CS cycles

RF_fatigue = σ_admissible(N_certif × CS) / σ_appliquée

RF > 1 → nœud dans les marges
RF < 1 → nœud hors marge, retour au bureau d’études

Objectif de certification : N_certif cycles de vol
Coefficient de sécurité : CS (3 ou 5 selon criticité)
→ On vérifie à N_certif × CS cycles

RF_fatigue = σ_admissible(N_certif × CS) / σ_appliquée

RF > 1 → nœud dans les marges
RF < 1 → nœud hors marge, retour au bureau d'études
# Cas de fatigue uniquement
df_fatigue = df[df['cas_de_charge'] == 'LC5_Fatigue'].copy()

# Amplitude de contrainte appliquée
# R = 0.1 : rapport de charge typique structure aéro (σ_min / σ_max)
R = 0.1
df_fatigue['sigma_amplitude_mpa'] = df_fatigue['sigma_vm_mpa'] * (1 - R) / 2

# Table S-N de référence (simulant une courbe matériau certifiée)
# En pratique : fournie par le bureau matériaux, confidentielle
# Format : contrainte admissible pour un nombre de cycles donné
TABLE_SN = pd.DataFrame({
    'N_cycles'      : [1_000, 5_000, 10_000, 50_000,
                        100_000, 500_000, 1_000_000],
    'sigma_adm_mpa' : [350,    280,    240,    180,
                        150,    110,    90],
})

print("Courbe S-N de référence :")
print(TABLE_SN.to_string(
    index=False,
    formatters={
        'N_cycles'      : '{:>10,.0f}'.format,
        'sigma_adm_mpa' : '{:>15.0f}'.format,
    }
))

# Interpolation en échelle log-log
# (comme on lirait graphiquement une courbe de Wöhler)
interp_sn = interp1d(
    np.log10(TABLE_SN['N_cycles']),
    TABLE_SN['sigma_adm_mpa'],
    kind='linear',
    fill_value='extrapolate'
)
Courbe S-N de référence :
  N_cycles   sigma_adm_mpa
     1,000             350
     5,000             280
    10,000             240
    50,000             180
   100,000             150
   500,000             110
 1,000,000              90
# Paramètres de certification
N_CERTIF = 80_000   # objectif de durée de vie (cycles de vol)
CS       = 3        # coefficient de sécurité (3 non critique, 5 critique)

N_VERIFICATION    = N_CERTIF * CS
sigma_adm_verif   = float(interp_sn(np.log10(N_VERIFICATION)))

print(f"\nVérification fatigue :")
print(f"  Objectif certification : {N_CERTIF:,} cycles")
print(f"  Coefficient sécurité   : {CS}")
print(f"  Vérification à         : {N_VERIFICATION:,} cycles")
print(f"  σ admissible           : {sigma_adm_verif:.1f} MPa")

# Calcul du Reserve Factor fatigue pour chaque nœud
df_fatigue['sigma_adm_mpa'] = sigma_adm_verif
df_fatigue['RF_fatigue']    = (df_fatigue['sigma_adm_mpa']
                                / df_fatigue['sigma_amplitude_mpa'])

# Nœuds hors marge fatigue
hors_marge = df_fatigue[df_fatigue['RF_fatigue'] < 1.0].sort_values('RF_fatigue')
print(f"\nNœuds hors marge fatigue (RF < 1) : {len(hors_marge)}")

# Tableau synthèse — 10 nœuds les plus critiques
critique_fatigue = (
    df_fatigue[['noeud', 'sigma_amplitude_mpa',
                'sigma_adm_mpa', 'RF_fatigue']]
    .sort_values('RF_fatigue')
    .head(10)
)

print("\nTop 10 nœuds critiques en fatigue :")
print(
    critique_fatigue.to_string(
        index=False,
        formatters={
            'sigma_amplitude_mpa' : '{:.1f}'.format,
            'sigma_adm_mpa'       : '{:.1f}'.format,
            'RF_fatigue'          : '{:.3f}'.format,
        }
    )
)
Vérification fatigue :
  Objectif certification : 80,000 cycles
  Coefficient sécurité   : 3
  Vérification à         : 240,000 cycles
  σ admissible           : 128.2 MPa

Nœuds hors marge fatigue (RF < 1) : 7

Top 10 nœuds critiques en fatigue :
noeud sigma_amplitude_mpa sigma_adm_mpa RF_fatigue
N0047               172.2         128.2      0.745
N0050               154.5         128.2      0.830
N0019               147.5         128.2      0.869
N0022               143.5         128.2      0.894
N0018               138.2         128.2      0.928
N0029               137.2         128.2      0.934
N0023               132.8         128.2      0.966
N0024               126.4         128.2      1.015
N0006               121.3         128.2      1.057
N0004               120.3         128.2      1.066

En 2001, cette vérification se faisait nœud par nœud ; on lisait la contrainte admissible sur la courbe S-N papier, on calculait le RF à la main, on remplissait le tableau de marges dans Excel. Sur 50 nœuds critiques avec deux coefficients de sécurité, c’était une journée de travail. Avec ce pipeline cela prend quelques secondes et le tableau se régénère automatiquement si les résultats du calcul changent.


Partie 4 — Visualisation des résultats

Un bon post-traitement ne s’arrête pas aux tableaux. Il faut voir les données pour les comprendre et pour les présenter à un client ou un bureau de certification.

Graphique 1 — Reserve Factors statiques et fatigue par nœud :

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# RF statique : minimum par nœud sur tous les cas de charge
rf_statique = df.groupby('noeud')['RF'].min().reset_index()
rf_statique['couleur'] = rf_statique['RF'].apply(
    lambda x: '#e94560' if x < 1.0
              else '#f5a623' if x < 1.2
              else '#27ae60'
)

axes[0].barh(rf_statique['noeud'].tail(20),
             rf_statique['RF'].tail(20),
             color=rf_statique['couleur'].tail(20))
axes[0].axvline(x=1.0, color='black', ls='--', lw=1.5,
                label='RF = 1.0 (rupture)')
axes[0].axvline(x=1.2, color='gray',  ls=':', lw=1.0,
                label='RF = 1.2 (marge min.)')
axes[0].set_xlabel('Reserve Factor statique')
axes[0].set_title('RF statique minimum par nœud\n'
                   '(Rouge < 1.0 | Orange < 1.2 | Vert OK)')
axes[0].legend(fontsize=8)
axes[0].grid(True, alpha=0.3, axis='x')

# RF fatigue : distribution sur tous les nœuds
rf_fatigue_vals = df_fatigue['RF_fatigue']
couleurs_fatigue = rf_fatigue_vals.apply(
    lambda x: '#e94560' if x < 1.0
              else '#f5a623' if x < 1.2
              else '#27ae60'
)

axes[1].barh(df_fatigue['noeud'].tail(20),
             rf_fatigue_vals.tail(20),
             color=couleurs_fatigue.tail(20))
axes[1].axvline(x=1.0, color='black', ls='--', lw=1.5,
                label='RF = 1.0 (rupture fatigue)')
axes[1].set_xlabel('Reserve Factor fatigue')
axes[1].set_title(f'RF fatigue — vérification à {N_VERIFICATION:,} cycles\n'
                   f'(N_certif={N_CERTIF:,} × CS={CS})')
axes[1].legend(fontsize=8)
axes[1].grid(True, alpha=0.3, axis='x')

plt.tight_layout()
plt.savefig('post_traitement_FEM.png', dpi=150, bbox_inches='tight')
plt.show()
Graphique représentant les Reserve Factors statiques et fatigue par noeud.

Graphique 2 — Courbe de Wöhler tabulée avec positionnement des nœuds :

fig, ax = plt.subplots(figsize=(10, 6))

# Courbe S-N de référence (tabulée — interpolée en log-log)
N_courbe = np.logspace(3, 6.5, 200)
sigma_courbe = interp_sn(np.log10(N_courbe))

ax.semilogx(N_courbe, sigma_courbe,
            color='#1a1a2e', lw=2.5,
            label='Courbe S-N de référence (tabulée)')

# Zone admissible (sous la courbe)
ax.fill_between(N_courbe, 0, sigma_courbe,
                color='#27ae60', alpha=0.06,
                label='Zone admissible')

# Nœuds du calcul EF positionnés sur la courbe
# Couleur selon RF : rouge hors marge, vert dans les marges
couleurs = df_fatigue['RF_fatigue'].apply(
    lambda x: '#e94560' if x < 1.0 else '#27ae60'
)
ax.scatter(
    [N_VERIFICATION] * len(df_fatigue),
    df_fatigue['sigma_amplitude_mpa'],
    c=couleurs, s=40, alpha=0.7, zorder=5,
    label='Nœuds EF (rouge = hors marge)'
)

# Lignes de vérification
ax.axvline(x=N_CERTIF,      color='#2980b9', ls=':', lw=1.5,
           label=f'Objectif certification ({N_CERTIF:,} cy)')
ax.axvline(x=N_VERIFICATION, color='#f5a623', ls='--', lw=1.5,
           label=f'Vérification avec CS={CS} ({N_VERIFICATION:,} cy)')
ax.axhline(y=sigma_adm_verif, color='#f5a623', ls='--', lw=1.0, alpha=0.5)

ax.set_xlabel('Nombre de cycles N (échelle log)')
ax.set_ylabel('Amplitude de contrainte σa (MPa)')
ax.set_title('Courbe de Wöhler — positionnement des nœuds EF')
ax.legend(fontsize=9)
ax.grid(True, which='both', alpha=0.3)
ax.set_ylim(bottom=0)

plt.tight_layout()
plt.savefig('courbe_wohler.png', dpi=150, bbox_inches='tight')
plt.show()
Courbe de Wöhler

Ce que cela change vraiment

Faisons le bilan honnête de ce que Python apporte sur ce type de post-traitement, par rapport à ce que je faisais en 2001.

En 2001 : 50 nœuds critiques, 5 cas de charge, calcul des RF et des durées de vie à la main dans Excel. Une journée de travail pour un ingénieur expérimenté. Risque d’erreur de saisie non négligeable.

Avec Python aujourd’hui : 50 nœuds, 5 cas de charge, RF, durées de vie, graphiques. 30 secondes d’exécution. Et si le calcul éléments finis change — nouveau jeu de résultats, nouveaux matériaux, nouveau seuil de certification — on relance le script. Pas besoin de tout refaire à la main.

Mais surtout, Python ne remplace pas la connaissance métier. Le choix du coefficient de sécurité, la lecture d’une courbe S-N certifiée, l’interprétation d’un RF à 1.05 sur un nœud de bord libre — ça, c’est le travail de l’ingénieur. Python s’occupe de tout le reste.


Et avec des logiciels comme Samcef ou Abaqus ?

La plupart des logiciels éléments finis modernes proposent des exports CSV ou TXT de leurs résultats — c’est le point d’entrée naturel pour ce pipeline Python. Certains (Abaqus, Nastran) ont même des APIs Python directes qui permettent d’interroger les résultats sans passer par un fichier intermédiaire.

Sur Samcef ou Safe, le chemin classique est l’export de tableaux de résultats en format texte. Une fois dans Python, le workflow décrit dans cet article s’applique directement.

La connexion directe Python ↔ logiciels EF (Abaqus ODB, Nastran OP2) fait l’objet d’un module avancé dans la formation.


💬 Vous post-traitez encore des résultats EF à la main dans Excel ? Ou vous avez déjà automatisé une partie du processus ? Dites-le moi en commentaire.

📩 Je prépare une formation complète sur la data science appliquée à l’industrie, pensée pour les ingénieurs. [Rejoignez la liste d’attente →]

Laisser un commentaire