Construire et calculer un modèle en Python

Sept objets couvrent presque tous les modèles : Node, Material, Section, Member, MemberSet, NodalSupport et NodalLoad. Vous les assemblez, appelez run_analysis() et lisez un jeu de résultats indexé par cas de charge.

Les objets principaux

Tout s’importe depuis fers_core. Un modèle est une instance FERS qui contient des groupes de barres ; les charges dépendent des cas de charge et non du modèle, si bien que la même géométrie peut porter plusieurs cas indépendants.

  • Node — un point X, Y, Z. Y est la verticale ascendante. Les nœuds portent leur condition d’appui.
  • Material — module d’élasticité, module de cisaillement, masse volumique, limite d’élasticité. Tout en SI : pascals et kg/m³.
  • Section — aire et moments quadratiques, rattachés à un matériau.
  • Member — un élément poutre à deux nœuds et six degrés de liberté par nœud, rattaché à une section.
  • MemberSet — le groupe que vous ajoutez au modèle, et l’unité sur laquelle portent les vérifications.
  • NodalSupport — la condition aux limites affectée à un nœud.
  • NodalLoad — une force ou un moment appliqué à un nœud au sein d’un cas de charge.
from fers_core import (
    FERS, Node, Member, Section, Material,
    MemberSet, NodalSupport, NodalLoad
)

Définir géométrie, matériau et section

Les coordonnées sont en mètres et les propriétés du matériau en pascals. La section ci-dessous est un IPE 180 issu de la bibliothèque de profilés FERS.

Notez la convention d’axes, car c’est de loin la cause la plus fréquente d’un résultat faux : i_z est l’axe fort et i_y l’axe faible. Une poutre qui fléchit dans son plan habituel est gouvernée par i_z.

# Nodes (x, y, z coordinates in metres)
node1 = Node(0, 0, 0)
node2 = Node(5, 0, 0)

# Material (Steel S235)
steel = Material(
    name="Steel S235",
    e_mod=210e9,     # Pa
    g_mod=80.769e9,  # Pa
    density=7850,    # kg/m³
    yield_stress=235e6  # Pa
)

# Cross-section (IPE 180), values from the FERS steel section library.
# Axis convention: i_z is the STRONG axis (bending in the local x-y plane),
# i_y the weak axis — see ferscloud.com/conventions
section = Section(
    name="IPE 180",
    material=steel,
    i_y=1.009e-6,  # m⁴ (weak axis)
    i_z=13.17e-6,  # m⁴ (strong axis)
    j=0.0477e-6,   # m⁴
    area=0.00240   # m²
)

Toutes les conventions d’axes, de signes et d’unités figurent sur la page des conventions. Le contrat JSON attend des angles en degrés, pas en radians.

Créer la barre, appliquer appui et charge

Un NodalSupport() sans argument est un encastrement complet. Pour une articulation ou un appui glissant, passez explicitement displacement_conditions et rotation_conditions — les exemples montrent les deux.

Les directions de charge sont des vecteurs unitaires globaux : (0, 1, 0) avec une intensité négative est donc une force vers le bas.

# Create beam element
beam = Member(start_node=node1, end_node=node2, section=section)

# Fixed support at node1
node1.nodal_support = NodalSupport()

# Organise members into a set and add to model
model = FERS()
model.add_member_set(MemberSet(members=[beam]))

# Load case with a −1 kN point load at node2
lc = model.create_load_case(name="Gravity")
NodalLoad(node=node2, load_case=lc, magnitude=-1000, direction=(0, 1, 0))

Lancer le calcul et lire les résultats

run_analysis() résout tous les cas de charge. Les résultats se trouvent sur model.resultsbundle, indexés par nom de cas, avec déplacements et réactions indexés par identifiant de nœud sous forme de chaîne.

Les commentaires montrent la sortie réelle pour ces entrées exactes.

model.run_analysis()

results = model.resultsbundle.loadcases["Gravity"]

dy = results.displacement_nodes["2"].dy
Vy = results.reaction_nodes["1"].nodal_forces.fy
Mz = results.reaction_nodes["1"].nodal_forces.mz

print(f"Tip deflection : {dy*1000:.3f} mm")   # -15.065 mm
print(f"Reaction Vy    : {Vy:.1f} N")         # 1000.0 N
print(f"Reaction Mz    : {Mz:.1f} Nm")        # 5000.0 Nm

Vérification manuelle : une console de 5 m fléchissant autour de son axe fort donne δ = PL³ / 3EI_z = 1000·5³ / (3 · 210×10⁹ · 13,17×10⁻⁶) = 15,07 mm, avec une réaction de 1 kN et un moment d’encastrement de 5 kN·m par équilibre. Le solveur renvoie 15,065 mm.

Lire, enregistrer et recharger les résultats

Depuis FERS 0.1.98, member_results et displacement_nodes sont des tables en lecture seule. Elles se lisent comme des dictionnaires — [], get, in, .items() —, mais chaque accès construit un nouvel objet non modifiable, et c’est ce qui garde un gros résultat léger en mémoire. .copy() renvoie un dictionnaire ordinaire d’objets modifiables.

save_to_json écrit le modèle avec ses résultats, et FERS.from_json relit tout fichier écrit par le SDK ou par le solveur, entrée par entrée. Pour un gros modèle, laissez le solveur écrire le fichier avec run_analysis_to_file : le résultat ne transite alors jamais par Python sous la forme d’une seule chaîne.

from fers_core import FERS

results = model.resultsbundle.loadcases["Gravity"]

# A read-only table: every lookup builds a fresh object
beam_result = results.member_results["1"]
print(beam_result.local_start_forces.mz)

editable = results.member_results.copy()   # a plain dict of editable objects
editable["1"].local_start_forces.mz = 0.0

model.save_to_json("cantilever.json")      # the model and its results
reloaded = FERS.from_json("cantilever.json")

# A large model: let the solver write the file itself
model.run_analysis_to_file("cantilever_results.json")
solved = FERS.from_json("cantilever_results.json")

Ne demander que les résultats que vous lisez

result_filter garde ou écarte des blocs entiers de résultats. result_requests (FERS 0.1.99) restreint un bloc aux barres ou nœuds, combinaisons de charges, composantes et extrémité de barre qu’une vérification lit réellement. Les valeurs arrivent dans les habituels member_results, displacement_nodes et reaction_nodes, et tout ce qui n’a pas été demandé se lit None : une vérification ne passe donc jamais grâce à un zéro que personne n’a calculé.

Les combinaisons de charges sont sélectionnées par leur limit_state, comme le font les vérifications de calcul : renseignez-le donc sur chaque combinaison. Les valeurs elles-mêmes sont dans resultsbundle.selections : un tableau par demande, de forme (jeux de résultats, identifiants, champs, composantes), chaque axe étant listé à côté.

from fers_core import ResultBlock, ResultRequest
from fers_core.loads.enums import LimitState
from fers_core.result_requests import nodes_of
from fers_core.unity_checks import members

# Requests select load combinations by limit state, as design checks do
uls = model.create_load_combination("ULS 1", {lc: 1.35}, situation="ULS", check="ALL")
uls.limit_state = LimitState.ULS

beams = members([beam.id])
model.settings.analysis_options.result_requests = [
    ResultRequest(ResultBlock.LOCAL_END_FORCES, members=beams,
                  limit_state="ULS", end="start", components=["fy", "mz"]),
    ResultRequest(ResultBlock.NODE_DISPLACEMENTS, nodes=nodes_of(beams),
                  limit_state="ULS", components=["dy"]),
]
model.run_analysis()

uls_results = model.resultsbundle.loadcombinations["ULS 1"]
start = uls_results.member_results["1"].local_start_forces
print(start.mz)   # requested
print(start.my)   # None: not requested

selection = model.resultsbundle.selections[0]
print(selection["fields"], selection["components"])
print(selection["values"].shape)   # (result sets, ids, fields, components)

Sur un modèle de rayonnage de 4 travées comptant 8 546 barres et 31 combinaisons de charges, le fichier de résultats est passé de 452 Mo à 41 Mo avec les seules valeurs que lisent ses vérifications.

Laisser une poutre continue tourner sur ce qu’elle croise

Un rail qui passe sur des poutres est continu sur toute sa longueur, mais libre de tourner sur chaque poutre. Un MemberHinge relâche une extrémité de barre : relâcher les deux extrémités du rail sur une poutre le couperait en deux travées. Donnez aux barres du rail leur propre groupe de barres avec une ScissorHinge (FERS 0.1.99) : le groupe reste lié à lui-même et tourne, ou glisse, sur chaque barre hors du groupe qu’il rencontre, sans barre de liaison ni nœud supplémentaire.

Chaque valeur agit comme un relâchement de rotule de barre : None bloque l’axe, 0.0 le relâche et une valeur positive est un ressort. Les axes sont globaux, sauf si l’articulation nomme un repère avec ReleaseAxes.user(x, y) ; scissor_hinge_nodes limite l’articulation à certains nœuds du groupe, et une MemberHinge peut prendre ses rotations autour d’axes globaux ou propres avec rotation_axes. Un modèle qui utilise l’un d’eux est écrit avec schema_version 3, si bien qu’un solveur plus ancien le refuse au lieu de calculer chaque liaison comme rigide.

from fers_core import FERS, DistributedLoad, Material, Member, MemberSet, NodalSupport, Node, ScissorHinge, Section
from fers_core.supports.supportcondition import SupportCondition as C

model = FERS()
steel = Material(name="S355", e_mod=210e9, g_mod=81e9, density=7850.0, yield_stress=355e6)
rail_section = Section(name="rail", material=steel, i_y=1e-6, i_z=1e-6, j=1e-7, area=1e-3)
beam_section = Section(name="beam", material=steel, i_y=1e-4, i_z=1e-4, j=1e-4, area=5e-3)

turns = {"X": C.fixed(), "Y": C.free(), "Z": C.free()}
a, m, b = Node(0.0, 0.0, 0.0), Node(1.0, 0.0, 0.0), Node(2.0, 0.0, 0.0)
a.nodal_support = NodalSupport(rotation_conditions=turns)
b.nodal_support = NodalSupport(
    displacement_conditions={"X": C.free(), "Y": C.fixed(), "Z": C.fixed()}, rotation_conditions=turns
)
beam_start, beam_end = Node(1.0, 0.0, -0.5), Node(1.0, 0.0, 0.5)
beam_start.nodal_support = beam_end.nodal_support = NodalSupport()  # clamped

rail_1 = Member(start_node=a, end_node=m, section=rail_section)
rail_2 = Member(start_node=m, end_node=b, section=rail_section)
model.add_member_set(MemberSet(
    members=[rail_1, rail_2],
    scissor_hinge=ScissorHinge(rotational_release_y=0.0, rotational_release_z=0.0),
))
model.add_member_set(MemberSet(members=[
    Member(start_node=beam_start, end_node=m, section=beam_section),
    Member(start_node=m, end_node=beam_end, section=beam_section),
]))

span = model.create_load_case(name="span")
DistributedLoad(member=rail_1, load_case=span, magnitude=1000.0, end_magnitude=1000.0, direction=(0, -1, 0))
model.run_analysis()
mz = model.resultsbundle.loadcases["span"].member_results[str(rail_1.id)].local_end_forces.mz
print(f"{abs(mz):.1f} Nm")  # 62.4; a knife edge gives qL²/16 = 62.5, a rigid joint 122.6

Vérification manuelle : un rail continu sur deux travées de 1 m, dont une chargée, a sur l’appui central un moment qL²/16 = 1000 · 1² / 16 = 62,5 N·m. Le solveur renvoie 62,4 N·m, la poutre cédant un peu sous le rail ; fixé rigidement à la poutre, le rail reprend 122,6 N·m.

Au-delà de la statique linéaire

Le même objet modèle porte les réglages des autres types d’analyse. L’analyse du second ordre (P-Delta), le flambement linéaire avec facteur de charge critique, l’analyse modale et l’analyse sismique par spectre de réponse se pilotent tous depuis les réglages d’analyse, et non depuis une autre API.

Les vérifications s’attachent aux groupes de barres et s’évaluent après le calcul ; elles renvoient des taux de travail clause par clause accompagnés des valeurs intermédiaires qui les ont produits — une vérification se lit donc comme un calcul manuel et non comme un nombre isolé.

  • Contrôlez d’abord contre des solutions analytiques — voir les tests de précision et le jeu NAFEMS.
  • Pour les vérifications EN 1993-1-1, check_beam construit, calcule et vérifie une travée en un seul appel — voir les exemples.

Pages associées

Voir aussi

Questions fréquentes

Pourquoi mes déplacements sont-ils indexés par une chaîne ?

Les dictionnaires de résultats sont indexés par identifiant de nœud et de barre sous forme de chaîne, parce que le jeu de résultats transite par JSON. C’est displacement_nodes["2"], pas [2].

Pourquoi la modification d’un résultat lève-t-elle une erreur ?

Depuis FERS 0.1.98, les résultats sont en lecture seule : chaque accès construit un nouvel objet, si bien qu’une modification serait perdue à l’accès suivant. Appelez .copy() sur member_results ou displacement_nodes pour obtenir un dictionnaire ordinaire d’objets modifiables.

À quoi sert un MemberSet ?

C’est l’unité que vous ajoutez au modèle et celle sur laquelle portent les vérifications. Regrouper des barres formant un même élément physique — un poteau éclissé en deux tronçons, par exemple — permet à une longueur de flambement ou à une vérification normative de couvrir l’ensemble.

Comment modéliser une articulation ou un appui glissant ?

Passez des conditions explicites à NodalSupport. Une articulation bloque les trois translations et laisse libres les rotations de flexion ; bloquez en plus la rotation de torsion, sinon la barre présente un mode de corps rigide et le système devient singulier. L’exemple de poutre sur deux appuis montre les deux.

Quel moment quadratique correspond à l’axe fort ?

i_z. FERS traite l’axe z local comme l’axe fort de flexion partout : solveur, vérifications et diagrammes sont cohérents. Intervertir i_y et i_z est l’erreur de modélisation la plus courante.

Faut-il une connexion Internet pour calculer ?

Non. run_analysis() exécute le solveur compilé dans votre propre processus. Le réseau n’est utilisé que si vous appelez un point de terminaison cloud.

Puis-je obtenir la déformée et pas seulement les extrémités ?

Oui, et elle est exacte vis-à-vis de la charge plutôt qu’interpolée entre les déplacements d’extrémité : une poutre bi-encastrée sous charge uniforme donne la flèche réelle wL⁴/384EI. Demandez-la dans les options d’analyse.