Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Python & conventions pyrucast

Un script pyrucast est un programme Python ordinaire : il n’y a pas de langage à part à apprendre. Cette page rassemble ce qu’il faut savoir pour lire et écrire les scripts du tutoriel, en deux parties :

  1. le Python des scripts — chaque notion de Python, dans l’ordre où on la rencontre, suivie aussitôt de ce que pyrucast en fait : ses raccourcis ;
  2. les conventions de pyrucast — où trouver une fonction, quand elle existe aussi en méthode, comment enchaîner les appels.

Inutile de tout retenir d’une traite : revenez ici quand une ligne d’un script vous arrête. Tous les extraits de cette page sont exécutés à chaque vérification du projet.

Partie 1 — Le Python des scripts, et les raccourcis de pyrucast

Un script, et comment le lancer

Un script est un fichier texte d’extension .py. Python le lit de haut en bas, une instruction par ligne. Tout ce qui suit un # est un commentaire : Python l’ignore, il est là pour le lecteur. Le texte entre triples guillemets """…""" en tête d’un script le présente.

On le lance depuis un terminal, dans le dossier du script :

python mon_script.py

On peut aussi taper les instructions une à une : la commande python seule ouvre une session interactive (invite >>>), où chaque ligne s’exécute dès qu’on appuie sur Entrée. Un notebook Jupyter fonctionne de la même façon, cellule par cellule.

Nommer des valeurs

COTE = 0.10  # un nombre décimal s'écrit avec un point, jamais une virgule
SOURCE = 1.0e6  # 1,0 × 10⁶ : l'exposant s'écrit après « e »
FLUX = -40_000.0  # les « _ » ne servent qu'à lire les grands nombres
DEMI_COTE = COTE / 2.0  # un calcul, rangé sous un nouveau nom

element = "QUA4"  # du texte, entre guillemets

Le signe = range une valeur sous un nom : COTE vaut désormais 0.10, et on écrit COTE partout où l’on veut cette valeur. Un nombre décimal prend un point ; 1.0e6 se lit « 1,0 × 10⁶ » ; les _ d’un grand nombre ne servent qu’à le lire. Les noms en majuscules sont une convention, pas une règle : ils signalent les données du calcul, posées une fois en tête du script.

Le texte s’écrit entre guillemets. pyrucast en reçoit beaucoup : un type d’élément ("QUA4"), un nom de composante ("T"), un nom de coefficient ("k").

Regrouper des valeurs : listes, tuples, dictionnaires

point = [0.0, 0.10]  # une liste : les coordonnées x, y d'un point
rouge = (255, 0, 0)  # un tuple : une couleur, rouge, vert, bleu
coefficients = [("k", 50.0), ("phi_q", 1.0e6)]  # une liste de paires (nom, valeur)
LONGUEUR, HAUTEUR = 0.30, 0.10  # deux noms d'un coup

print(point[0])  # 0.0 — le premier élément porte le numéro 0
print(coefficients[1])  # ('phi_q', 1000000.0)
  • Une liste, entre crochets […], aligne des valeurs : les coordonnées d’un point, les composantes d’un vecteur, une suite d’instants.
  • Un tuple, entre parenthèses (…), fait de même pour un groupe qui ne change pas : une couleur (rouge, vert, bleu), un point de vue.
  • Une liste de paires (nom, valeur) est la forme sous laquelle pyrucast reçoit les coefficients d’un matériau.
  • Les éléments se numérotent à partir de 0 : point[0] est le premier.
donnees = {"times": [0.0, 0.5, 1.0]}  # des valeurs rangées par nom
donnees["save_times"] = [1.0]  # une entrée de plus
print(donnees["times"])  # [0.0, 0.5, 1.0]

Un dictionnaire, entre accolades {…}, range des valeurs par nom. C’est sous cette forme que le calcul pas à pas reçoit ses données.

Importer pyrucast

import os

import pyrucast as pc  # le paquet, sous le nom court « pc »

coords = pc.Coords(2)  # pc.Coords : la classe Coords du paquet
a = coords.add_node([0.0, 0.0])
b = coords.add_node([0.10, 0.0])
ligne = pc.mesh.line(a, b, 20)  # pc.mesh.line : la fonction line du module mesh

import pyrucast as pc rend tout pyrucast disponible sous le nom court pc. Le point se lit « qui appartient à » : pc.Coords est la classe Coords du paquet, pc.mesh.line la fonction line de son module mesh.

import os charge un module livré avec Python : les scripts du tutoriel s’en servent pour lire une variable d’environnement et composer des chemins de fichiers.

Un script peut aussi reprendre des noms d’un autre script du même dossier : tutoriel/thermique.py commence par from maillage import HEIGHT, HOLE_RADIUS, LENGTH, OUT, THICKNESS, show, structured_mesh, et calcule ainsi sur le maillage construit par tutoriel/maillage.py, sans le recopier. C’est pourquoi les deux fichiers doivent rester côte à côte.

Appeler une fonction

plaque = pc.mesh.extrude(ligne, [0.0, 0.10], 20)  # trois arguments, dans l'ordre
cotes = pc.mesh.border(plaque, angle_deg=45)  # un argument donné par son nom
bord = pc.mesh.border(plaque)  # sans lui : sa valeur par défaut

Une fonction s’appelle avec ses arguments entre parenthèses :

  • par position, dans l’ordre que la fonction attend — pour extrude, le maillage à balayer, le vecteur, le nombre de couches ;
  • par nom, angle_deg=45, quand l’ordre ne suffit pas à se faire comprendre ou que l’argument est facultatif ;
  • un argument facultatif omis prend sa valeur par défaut : sans angle_deg, border rend tout le bord d’un seul tenant, au lieu de le couper en quatre côtés.

Trois valeurs reviennent souvent : True (vrai), False (faux), et None, qui veut dire « rien » — par exemple « pas de fichier, ouvre une fenêtre ».

Pour savoir ce qu’attend une fonction, on demande son aide dans une session interactive :

>>> help(pc.mesh.line)
line(a, b, n_elems, element_type=None)
    Build a line of `n_elems` elements from node `a` to node `b`.

    `element_type` is `"SEG2"` (default) or `"SEG3"`.

Les éditeurs de code (VS Code, PyCharm…) affichent la même aide, et proposent les noms de fonctions et d’arguments au fil de la frappe.

Les objets : méthodes et propriétés

print(plaque.cell_count)  # 400 — une propriété : sans parenthèses
print(plaque.element_types())  # ['QUA4'] — une méthode : avec parenthèses
print(coords.node_count)  # 441
plaque[0].face_color = (255, 0, 0)  # une propriété qui se modifie

Un objet — un magasin de nœuds, un maillage, un champ — porte avec lui deux sortes de choses, qu’on atteint par un point :

  • des méthodes, qui agissent : elles s’appellent avec des parenthèses, plaque.element_types() ;
  • des propriétés, qui sont des valeurs : sans parenthèses, plaque.cell_count. Les nombres que l’on compte (mailles, nœuds, composantes) sont des propriétés dans pyrucast.

Certaines propriétés se modifient : face_color fixe la couleur de tracé d’une zone de maillage. Les scripts du tutoriel s’en servent pour colorier les régions chargées.

Les crochets : zones, composantes, valeurs

La plupart des objets pyrucast sont découpés en zones : un maillage a une zone par type d’élément ou par morceau, un champ une zone par support, un modèle une zone par terme. Les crochets, qui en Python désignent un élément d’une liste, désignent ici une zone :

print(len(cotes))  # 4 — les quatre côtés de la plaque, une zone chacun
bas = cotes[0]  # la première zone
dernier = cotes[-1]  # la dernière
sauf_bas = cotes[1:]  # toutes sauf la première : encore un maillage
milieu = cotes[1:-1]  # ni la première ni la dernière

seulement_T = temperature["T"]  # un nom entre crochets : une composante

champ = pc.NodeField(ligne, ["T"])  # un champ nul aux nœuds de la ligne
champ[0][a, "T"] = 20.0  # une valeur : la zone, puis (nœud, composante)
print(champ[0][a, "T"])  # 20.0
  • x[0] est la première zone, x[-1] la dernière ; len(x) en donne le nombre.
  • x[1:] (« de la deuxième à la fin ») ou x[1:-1] (« ni la première ni la dernière ») rendent un objet de même nature, réduit à ces zones.
  • Une zone n’est pas une copie : la modifier, c’est modifier l’objet entier.
  • Sur un champ, un nom entre crochets garde une seule composante : temperature["T"] laisse de côté "lambda", les multiplicateurs de Lagrange que la résolution ajoute. Une liste de noms en garde plusieurs.
  • Sur une zone de champ, [nœud, composante] lit ou écrit une valeur.

Le détail est dans Champ.

Les opérateurs : réunir et calculer

deux_points = a | b  # deux nœuds : un maillage POI1 de deux points
bas_et_droite = cotes[0] | cotes[1]  # deux zones : un maillage à deux zones

kelvin = temperature["T"] + 273.15  # un nombre ajouté à toutes les valeurs
double = 2.0 * temperature["T"]  # toutes les valeurs multipliées
chaud = temperature["T"] >= 10.0  # 1 là où T ≥ 10 °C, 0 ailleurs

Sur des nombres, + - * / ** (la puissance) font ce qu’on attend. pyrucast leur donne un sens sur ses objets :

  • | réunit : deux nœuds en un maillage POI1, deux maillages en un maillage à plusieurs zones, deux modèles en un seul modèle, deux champs en un seul champ. Il juxtapose, il ne somme jamais (voir plus bas, Réunir sans sommer).
  • + - * / ** calculent sur les valeurs d’un champ, avec un nombre ou avec un autre champ posé sur les mêmes nœuds : temperature + 273.15 passe en kelvins, u + du met à jour un déplacement. -champ change le signe. Les règles, entre deux champs, sont dans Champ.
  • >=, >, <=, < comparent un champ à un nombre et rendent un masque : un champ qui vaut 1 là où la comparaison est vraie, 0 ailleurs.
  • Les matrices s’additionnent et se multiplient ou se divisent par un nombre, et K * u multiplie une matrice par un champ : c’est ainsi que s’écrit le pas en temps C / dt + K de la conduction transitoire.

Répéter : for, et l’indentation

for cote in cotes:
    print(cote.element_type, cote.cell_count)  # SEG2 20, quatre fois

PALETTE = [(255, 0, 0), (0, 170, 0), (0, 0, 255)]
for i, cote in enumerate(cotes):
    cote.face_color = PALETTE[i % len(PALETTE)]  # % : le reste de la division

for cote in cotes: répète le bloc qui suit pour chaque zone de cotes, en la rangeant à chaque tour sous le nom cote. Le bloc répété est celui qui est décalé vers la droite (quatre espaces) : en Python, l’indentation fait partie du langage. Un décalage oublié est une erreur, un décalage en trop change le sens du programme.

enumerate donne en plus le numéro de chaque tour, 0, 1, 2… ; % est le reste de la division, qui fait ici tourner les couleurs de la palette — c’est exactement ce que fait tutoriel/maillage.py pour colorier les faces d’une peau. Pour répéter un bloc n fois, on écrit for i in range(n):, où i vaut 0, 1, …, n - 1.

Les scripts du calcul pas à pas remplissent des listes dans des boucles :

instants = []  # une liste vide…
for i in range(3):
    instants.append(i * 150.0)  # …qui grandit d'un élément à chaque tour
print(instants)  # [0.0, 150.0, 300.0]

couples = [(0.0, 20.0), (150.0, 35.0)]
for t, valeur in couples:  # chaque couple, défait en deux noms
    print(t, valeur)

for avant, apres in zip([20.0, 35.0], [21.0, 37.0]):  # deux listes à la fois
    print(apres - avant)  # 1.0, puis 2.0

print(max([2, 1, 2]), sum([2, 1, 2]))  # 2 5

liste.append(x) ajoute x au bout de la liste : c’est ainsi que tutoriel/thermique_transitoire.py construit la liste de ses instants, et une liste de couples (temps, température) par point suivi. Quand chaque élément est un couple, for t, valeur in couples: le défait en deux noms. zip(a, b) parcourt deux listes ensemble, un élément de chacune à chaque tour — le script du rayonnement s’en sert pour lire, instant par instant, ses deux calculs côte à côte. max(liste) et sum(liste) donnent le plus grand élément et la somme.

Choisir : if

OUT = os.environ.get("PYRUCAST_TUTORIEL_IMG_DIR")  # None si la variable n'existe pas
if OUT:
    print("les figures seront écrites dans", OUT)
else:
    print("les figures s'ouvriront dans une fenêtre")

if n’exécute son bloc (décalé, lui aussi) que si la condition est vraie ; else donne l’autre branche. None, 0 et un texte vide comptent pour « faux ». Les scripts du tutoriel l’emploient pour choisir entre une fenêtre et un fichier : os.environ.get lit une variable d’environnement — un réglage posé dans le terminal avant de lancer le script, ici le dossier où écrire les figures — et rend None si elle n’existe pas.

Définir une fonction : def

Quand un même geste revient souvent, on le range dans une fonction. Voici celle que tutoriel/maillage.py définit pour tracer ses figures :

# FR — Répertoire des figures : défini → export SVG, absent → fenêtre interactive.
# EN — Figure directory: set → SVG export, unset → interactive window.
OUT = os.environ.get("PYRUCAST_TUTORIEL_IMG_DIR")

# FR — Vue commune à toutes les figures : azimut, élévation, échelle.
# EN — View shared by every figure: azimuth, elevation, scale.
VUE = (-45, 25, 1.0)

# FR — Une couleur par face plane de la peau (recyclée s'il y en a plus).
# EN — One colour per flat skin face (recycled if there are more of them).
PALETTE = [(255, 0, 255), (0, 255, 0), (0, 0, 255), (255, 0, 0), (255, 255, 0)]


def show(mesh: pc.Mesh, title: str, file: str, wireframe: bool = False) -> None:
    """FR — Trace `mesh` : fenêtre interactive, ou SVG si `OUT` est défini.

    EN — Plot `mesh`: interactive window, or SVG when `OUT` is set.
    """
    mesh.plot(
        view=VUE,
        title=title,
        wireframe=wireframe,
        save=os.path.join(OUT, file) if OUT else None,
    )


  • def show(…): définit la fonction show ; son corps est le bloc décalé.
  • mesh, title, file, wireframe sont ses paramètres ; wireframe: bool = False lui donne une valeur par défaut.
  • : pc.Mesh, : str, -> None disent quels types sont attendus et rendus. Ce sont des indications pour le lecteur et l’éditeur : Python ne les vérifie pas.
  • x if condition else y est un choix en une ligne : le chemin du fichier si OUT est défini, None (donc une fenêtre) sinon.
  • Une fonction rend un résultat par return. structured_mesh en rend deux, return grid, volume_structure, que tutoriel/thermique.py reçoit par _, volume = structured_mesh(plot=False) — le nom _ signale une valeur dont on ne se sert pas.

Chaque script range enfin son travail dans une fonction main(), et se termine par :

if __name__ == "__main__":
    main()

Cette condition n’est vraie que si l’on lance le fichier. Quand un autre script l’importe, main() ne s’exécute pas : tutoriel/thermique.py reprend le maillage de tutoriel/maillage.py sans retracer toutes ses figures.

Un bloc dans un contexte : with

with exécute un bloc décalé dans un contexte, et le défait à la sortie, même si le bloc s’arrête sur une erreur. pyrucast s’en sert pour calculer dans une autre configuration des coordonnées — la pièce déformée plutôt que la référence :

import pyrucast

c = pyrucast.Coords(dim=2)
n = c.add_node([1.0, 0.0])
deformee = c.add_config("deformed")

# Le bloc voit `deformee` ; la configuration d'avant revient à la sortie,
# même après une exception.
with c.in_config(deformee):
    n.set_position([1.5, 0.0])
    x = n.position()

print(x)  # [1.5, 0.0]
print(n.position())  # [1.0, 0.0] — la référence, de nouveau active

C’est un usage avancé, dont le tutoriel ne se sert pas : voir Coordonnées.

Afficher : print

print(…) écrit dans le terminal. Un texte précédé de f, une f-string, y insère des valeurs entre accolades : print(f"T max = {temperature.max('T'):.2f} °C") affiche la température maximale avec deux chiffres après la virgule.

Tout objet pyrucast s’affiche à trois niveaux de détail :

print(ligne)  # Mesh: 1 submesh(es) [#…], 20 cell(s) total — le résumé
print(repr(ligne))  # la structure : chaque zone, son type, son nombre de mailles
ligne.dump()  # tout le contenu, maille par maille

print(x) donne le résumé d’une ligne, repr(x) la structure, x.dump() le contenu complet — voir Aspect informatique.

Lire un message d’erreur

Quand une instruction échoue, Python s’arrête et affiche un message de ce genre :

Traceback (most recent call last):
  File "mon_script.py", line 14, in <module>
    bas = cotes.unit()
RuntimeError: expected a unitary Mesh (exactly one submesh(es)), found 4

Il se lit par le bas. La dernière ligne dit ce qui ne va pas — le type de l’erreur, puis le message, en anglais. Les lignes au-dessus disent où : le fichier, le numéro de ligne, l’instruction. Ici, unit() rend la zone unique d’un maillage, et cotes en a quatre : il fallait choisir une zone, cotes[0]. Les messages de pyrucast disent ce qui était attendu et ce qui a été trouvé.

Partie 2 — Les conventions de pyrucast

Trois sortes d’objets

  • Coords, le magasin de nœuds. Un script en crée un, en général, et tous les mailleurs y déposent leurs nœuds : c’est ce qui fait que deux maillages construits côte à côte partagent leurs nœuds communs.
  • Node, un nœud de ce magasin.
  • Les conteneurs, découpés en zones, chacun avec sa page dans la partie Objets :
objetcontientune zone
Meshun maillageSubMesh
FiniteElementSpacel’interpolation choisie sur un maillageSubFiniteElementSpace
NodeFielddes valeurs aux nœudsSubNodeField
ElementFielddes valeurs aux points de GaussSubElementField
Modelune physique, ses conditions, ses chargementsSubModel
Matrixune matrice creuseSubMatrix
Evolutiondes valeurs tabulées (une courbe, un champ qui évolue)SubEvolution

Un objet se déclare à partir d’une description, et naît souvent vide :

vide = pc.Mesh(coords, "TRI3")  # un maillage de triangles, encore sans maille
fes = pc.FiniteElementSpace(plaque)  # l'espace éléments finis de la plaque
nul = pc.NodeField(ligne, ["T"])  # un champ nul aux nœuds de la ligne

Tout le reste se calcule, par des fonctions.

Les fonctions, rangées par ce qu’elles produisent

Une fonction vit dans le module qui porte le nom de ce qu’elle rend. Pour trouver une fonction, demandez-vous ce que vous voulez obtenir :

modulerendexemples du tutoriel
pyrucast.meshun maillageline, arc, circle, extrude, sweep, triangulate_surface, skin, border, select, elements_on, points_on_cylinder, to_poi1
pyrucast.node_fieldun champ aux nœudspositions, restrict, external_forces
pyrucast.element_fieldun champ aux points de Gaussmaterial_field, gradient, integrate_behavior
pyrucast.modelun modèleheat_conduction, boundary_transfer, flux, dirichlet, elasticity
pyrucast.matrixune matricestiffness, mass

pyrucast.mesh.select rend un maillage — les nœuds retenus — bien qu’il parte d’un champ : c’est le résultat qui décide du module.

Les fonctions qui ne rendent aucun de ces objets sont rangées par activité :

moduleactivitéexemple
pyrucast.solverrésoudre un systèmesolve
pyrucast.measuremesurer un nombre (une intégrale…)integral
pyrucast.exportécrire pour un autre logicielexport_vtk
pyrucast.coordsdéplacer les nœuds du magasindisplace
pyrucast.fieldles fonctions mathématiques, sur tout champsqrt, cos, exp
pyrucast.steppingle calcul pas à passtep_by_step

Restent, au niveau du paquet lui-même, pyrucast.save et pyrucast.load, qui écrivent et relisent une session (voir Sauvegarde).

La même opération en méthode

Beaucoup de fonctions existent aussi en méthode de leur premier argument :

plaque_f = pc.mesh.extrude(ligne, [0.0, 0.10], 20)  # la fonction
plaque_m = ligne.extrude([0.0, 0.10], 20)  # la même, en méthode de `ligne`

Les deux écritures font exactement la même chose, et la méthode a la même aide que la fonction. Une fonction a sa méthode quand trois conditions sont réunies :

  1. son premier argument est l’objet que l’opération transforme ;
  2. elle rend un objet pyrucast ;
  3. elle a un sens pour n’importe quel objet de ce type.
fonctionméthode
pyrucast.mesh.extrude(ligne, v, n)ligne.extrude(v, n)
pyrucast.mesh.border(plaque)plaque.border()
pyrucast.mesh.skin(volume)volume.skin()
pyrucast.mesh.consolidate(m)m.consolidate()
pyrucast.mesh.to_poi1(m)m.to_poi1()
pyrucast.node_field.positions(m, ["X"])m.positions(["X"])
pyrucast.mesh.select(champ, ge=…)champ.select(ge=…)
pyrucast.node_field.restrict(champ, m)champ.restrict(m)
pyrucast.element_field.material_field(modele, …)modele.material_field(…)
pyrucast.node_field.external_forces(modele, mat)modele.external_forces(mat)
pyrucast.matrix.stiffness(modele, mat)modele.stiffness_matrix(mat)
pyrucast.solver.solve(K, f)K.solve(f)

Quand le module précisait la nature du résultat, la méthode la reprend dans son nom : pyrucast.matrix.stiffness devient stiffness_matrix.

Restent seulement des fonctions :

  • les physiques de pyrucast.model : leur premier argument est l’espace sur lequel on déclare la physique, pas un objet qu’elles transforment ;
  • pyrucast.mesh.line(a, b, n), dont le premier argument est un nœud ;
  • pyrucast.element_field.deformation, qui n’a de sens que pour un champ portant des déplacements u_x, u_y… — pas pour n’importe quel champ.

Composer les appels : imbriquer ou enchaîner

Le résultat d’un appel peut servir directement d’argument au suivant. Avec les fonctions, les appels s’imbriquent, et se lisent de l’intérieur vers l’extérieur :

plaque = pc.mesh.extrude(pc.mesh.line(a, b, 20), [0.0, 0.10], 20)
bord = pc.mesh.to_poi1(pc.mesh.border(plaque))

Avec les méthodes, ils s’enchaînent, et se lisent de gauche à droite, dans l’ordre des opérations — « une ligne, balayée ; son bord, en nœuds » :

plaque = pc.mesh.line(a, b, 20).extrude([0.0, 0.10], 20)
bord = plaque.border().to_poi1()

La résolution du premier calcul, des deux façons :

temperature = pc.solver.solve(
    pc.matrix.stiffness(modele, materiaux),
    pc.node_field.external_forces(modele, materiaux),
)
second_membre = modele.external_forces(materiaux)
temperature = modele.stiffness_matrix(materiaux).solve(second_membre)

Et deux lignes de tutoriel/thermique.py, écrites en méthodes (la forme fonction est rappelée en commentaire) :

# thermique.py : pc.mesh.consolidate(pc.mesh.skin(volume))
peau = volume.skin().consolidate()

# thermique.py : pc.mesh.select(pc.node_field.positions(peau, ["Z"]), ge=-TOL, le=TOL)
noeuds_bas = peau.positions(["Z"]).select(ge=-TOL, le=TOL)

Les scripts du tutoriel écrivent tout en fonctions, une opération par ligne : c’est la forme que décrit la documentation de chaque opérateur. Les deux formes se mélangent librement ; choisissez celle qui se lit le mieux.

Réunir sans sommer : | et les zones

conduction = pc.model.heat_conduction(fes)
modele = conduction | pc.model.dirichlet(bord, conduction, "T")
modele = modele | pc.model.flux(fes, modele, "q")
print(len(modele))  # 3 — trois termes réunis par |

| ajoute chaque terme au modèle comme une zone de plus : len(modele) en compte trois. unit() rend la zone unique d’un objet qui n’en a qu’une — et s’arrête sur une erreur sinon, comme plus haut. pyrucast.mesh.consolidate fond en une seule les zones d’un maillage qui ont le même type d’élément ; plusieurs mailleurs l’exigent.

Sur les champs, | juxtapose aussi les zones : à un nœud que deux zones partagent, il ne somme pas les valeurs. Pour additionner deux chargements qui se touchent, on les ramène d’abord sur les mêmes nœuds avec pyrucast.node_field.restrict, puis on les somme par + — c’est l’objet du passage Trois chargements qui se touchent de la page Régime stationnaire.

Les noms des composantes

print(conduction.dual_of("T"))  # q — la duale de la température : le flux
blocage = pc.model.dirichlet(bord, conduction, "T")  # on bloque la primale « T »

mecanique = pc.model.elasticity(fes, "plane_stress")
print(mecanique.dual_of("u_x"))  # f_x — la duale du déplacement : la force
appui = pc.model.dirichlet(bord, mecanique, "u_x")  # on bloque la primale « u_x »

Chaque physique déclare ses inconnues par paires : une variable primale, l’inconnue elle-même, et sa duale, la grandeur qui lui répond — T et le flux q en thermique, u_x et la force f_x en mécanique. dirichlet reçoit le nom de la primale, celle que l’on impose, et trouve la duale dans le modèle visé ; dual_of la donne.

Les coefficients des matériaux se désignent eux aussi par leur nom : « k » pour la conduction, « phi_q » pour une densité de flux, « h_T » et « a_ext_T » pour la convection. pyrucast.element_field.material_field sait lesquels le modèle réclame. Chaque page de Détails des physiques donne la liste des siens.

Suite : Maillage.