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

Opérateurs sur les champs

Les modules ops::node_field, ops::element_field, ops::coords, ops::measure et ops::field dérivent et transforment les champs — chacun nommé d’après ce qu’il produit : coordonnées, restriction, fusion, et dérivations géométriques vers les points de Gauss. Ces opérateurs croisent des conteneurs (maillage + champ, espace EF

  • champ) — ce sont donc des fonctions libres. L’arithmétique scalaire et par composante, elle, reste sur les types de champ (cf. Champ).

Les autres thèmes d’opérateurs ont leur propre page : construction du matériau (Construction), Assemblage (dont le chargement réparti flux), Comportement, Solveur.

Coordonnées et déplacement

PythonEffet
positions(mesh, components=None)un NodeField portant les coordonnées des nœuds ("X", "Y", "Z"), une zone par sous-maillage. None ⇒ tous les axes présents dans la dimension du Coords.
coords.set(field, components=None)écrit les coordonnées du Coords actif depuis un champ "X"/"Y"/"Z".
displace(field, components=None)ajoute un champ de déplacement aux coordonnées (chaque nœud distinct traité une seule fois).

positions est le pont géométrie → champ : on en tire un NodeField qu’on peut tracer, dériver, ou réinjecter après calcul (displace pour passer à la configuration déformée).

Restriction, fusion, consolidation

PythonEffet
restrict(field, mesh)restreint un NodeField aux nœuds de mesh (une zone par sous-maillage cible). Le support est le nuage POI1 canonique du sous-maillage (to_poi1, matérialisé une fois et mis en cache) : deux restrictions sur le même mesh partagent le support ⇒ restrict(a,mesh) - restrict(b,mesh) se soustrait directement, et s’aligne avec K·restrict(f,mesh) / solve(K,f). Pour les ops élément (gradient, integral, …), repasser mesh à côté. 0.0 pour les nœuds non couverts ; nœuds hors de mesh abandonnés. Erreur si mesh n’est pas sur le même Coords.
restrict_like(field, target)reprojette field sur le support et les composantes de target, zone par zone (mêmes slots que target) ⇒ le résultat se combine directement avec target par les opérateurs + - * /. Nœuds/composantes de field absents de target abandonnés ; 0.0 si non couverts. Typiquement pour replier un incrément de solve (qui porte aussi les multiplicateurs) dans une solution courante. Erreur si Coords différents.
merge(a, b)union structurelle de deux NodeField, consolidée — c’est l’alias nommé de a | b.
node_field.consolidate(field)fusionne les zones de même support (handle identique) en vérifiant la cohérence des valeurs partagées.
element_field.consolidate(field)fusionne les zones d’une même FiniteElementSpace (union des composantes) — le pendant de `

La consolidation d’un NodeField est exactement la finalisation de l’union | : après déduplication par handle, les zones définies sur le même SubMesh deviennent une seule zone portant l’union de leurs composantes — une composante définie par plusieurs zones doit y avoir la même valeur partout (sinon erreur). Une vérification inter-supports finale impose qu’un nœud partagé par des zones de supports différents s’accorde sur toute composante commune. Le même consolidate accepte un ElementField (opération element_field.consolidate) : les sous-champs d’une même FiniteElementSpace fusionnent en une zone portant l’union de leurs composantes — utile pour réunir des zones matériau bâties par physique sur une fespace partagée (k thermique + E/nu/alpha mécanique) en un champ matériau unique lu par chaque physique.

Bande de valeurs (ge / gt / le / lt)

select et mask partagent la même bande de valeurs, fixée par quatre bornes de comparaison qui reprennent une pour une les opérateurs Python :

ArgumentTestOpérateur
gev ≥ ge>=
gtv > gt>
lev ≤ le<=
ltv < lt<

On donne au plus une borne basse (ge ou gt) et au plus une borne haute (le ou lt), et au moins une borne en tout ; une borne absente laisse ce côté ouvert. Erreur si aucune borne, ou si la borne basse dépasse la haute.

Sélection par valeur

select extrait, zone par zone, la partie du support d’un champ dont les valeurs tombent dans la bande. C’est un filtre par valeur qui renvoie un Mesh — un sous-maillage par zone traitée (les zones restent séparées, rien n’est moyenné ni fusionné).

PythonEffet
select(field, ge=None, gt=None, le=None, lt=None, components=None)sous-ensemble du support du champ respectant la bande, une zone à la fois.
  • Type de champ. Sur un NodeField / SubNodeField, on sélectionne les nœuds : chaque zone donne un sous-maillage POI1 des nœuds retenus. Sur un ElementField / SubElementField, on sélectionne les cellules : chaque zone donne un sous-maillage de son propre type d’élément, et une cellule n’est retenue que si tous ses points de Gauss passent (la bande doit tenir tout le long de la cellule).
  • Composantes. components=None teste toutes les composantes de chaque zone. Une liste components ne teste que ces composantes, et seulement sur les zones qui les portent toutes — une zone à laquelle il manque une composante demandée est ignorée (aucun sous-maillage produit).
  • Combinaison (ET). Quand plusieurs composantes sont testées, elles sont combinées en ET : un nœud / une cellule n’est retenu que si chaque composante testée est dans la bande.
# Nodes whose temperature lies between 20 and 80 °C (inclusive bounds).
chauds = pyrucast.mesh.select(temperature, ge=20.0, le=80.0)

# Cellules dont la contrainte de von Mises dépasse un seuil (borne basse seule).
critiques = pyrucast.mesh.select(sigma, ge=250e6, components=["vm"])

Masque par valeur

mask garde la structure exacte du champ (mêmes zones, même support, mêmes composantes) et se contente de réécrire les valeurs : 1.0 là où la bande tient, 0.0 sinon — composante par composante (le MASQUE de Cast3M). Le résultat est donc du même type que l’entrée et se multiplie terme à terme avec elle. Un NodeField est masqué par nœud, un ElementField par point de Gauss.

PythonEffet
mask(field, ge=None, gt=None, le=None, lt=None, components=None)champ 0/1 de même structure que l’entrée.
  • Pas de ET entre composantes (contrairement à select) : chaque valeur est testée pour elle-même.
  • Composantes. components=None teste toutes les composantes. Une liste components ne teste que celles-ci ; les autres restent à 1.0 (neutre pour le produit), et une zone à laquelle il manque une composante demandée reste tout à 1.0.
# Resets a field's negative values to zero, component by component.
positif = champ * champ.mask(ge=0.0)

# Sugar: comparisons build a mask directly.
positif = champ * (champ >= 0.0)  # the same thing
chauds = temperature > 80.0  # NodeField 0/1

Les opérateurs >=, >, <=, < sur un champ (NodeField, SubNodeField, ElementField, SubElementField) renvoient le masque correspondant contre le scalaire de droite. == / != gardent leur sens Python habituel (identité).

Exemple complet et exécutable : examples/field_mask.py (lancer avec python examples/field_mask.py après maturin develop).

Extraction et renommage de composantes (EXCO)

Deux opérateurs travaillent sur le jeu de composantes d’un champ, sans toucher au support ni aux valeurs — l’équivalent de EXCO de Cast3M. Tous deux acceptent les quatre saveurs (NodeField, SubNodeField, ElementField, SubElementField) et renvoient la même saveur.

PythonEffet
filter_components(field, components)ne garde que les composantes nommées, zone par zone. components est un nom (str) ou une liste de noms — typiquement le résultat de model.primal_vars().
rename_component(field, old, new)renomme la composante old en new (métadonnée seule, aucune valeur déplacée).

filter_components traite chaque zone indépendamment :

  • une zone ne portant aucune des composantes demandées est abandonnée ;
  • une zone ne portant que des composantes demandées (rien à retirer) voit son sous-champ partagé tel quel (handle copié, pas de duplication) ;
  • une zone mixte est reconstruite sur le même support avec les seules composantes demandées, dans son propre ordre.

components peut être un sur-ensemble des composantes du champ (les noms absents sont ignorés) : passer model.primal_vars() à un résultat de solve pour en retirer les inconnues duales (multiplicateurs de Lagrange) est l’usage visé. Erreur si aucune zone ne porte l’une des composantes demandées.

rename_component laisse inchangée (handle partagé) toute zone ne portant pas old. Erreur si aucune zone ne porte old, ou si une zone concernée a déjà une composante nommée new.

# Removes the Lagrange multipliers from a solve result.
u = solution.filter_components(model.primal_vars())

# Renames a component before exporting.
export = u.rename_component("u_x", "DX")

Sucre d’indexation (façon pandas/numpy) : sur les quatre saveurs (NodeField, SubNodeField, ElementField, SubElementField), une clé chaîne ou liste de chaînes appelle filter_components et renvoie la même saveur. Les autres clés gardent leur sens : int/slice → accès aux zones sur un agrégat ; le tuple d’accès à une valeur sur un sous-champ (sub[node, "UX"], sub[cell, gauss, "E"]) est inchangé.

ux = champ["u_x"]  # == filter_components(champ, "u_x")
depl = champ[["u_x", "u_y"]]  # == filter_components(champ, ["u_x", "u_y"])
zone = champ[0]  # inchangé : la zone (SubNodeField)
val = champ[0][node, "u_x"]  # unchanged: the value at the node

L’accesseur champ.components() (présent sur les quatre saveurs) donne la liste des composantes, d’où l’idiome « reprojeter u1 sur les composantes de u2 » :

u = u1[u2.components()]  # u1 cut down to u2's set of components

Côté Rust, filter_components / select_components acceptent indifféremment un &str, un tableau ["u_x", "u_y"] ou un Vec<String> (trait IntoComponentNames) — donc field.filter_components(model.primal_vars()) passe directement.

Dérivation géométrique (vers les points de Gauss)

Ces opérateurs ne dépendent que de l’espace EF et du champ — aucune physique. Ils produisent l’ElementField que le comportement (integrate_behavior) consomme ensuite. Ils partagent tous le même moteur parallèle : le driver nodal_pointwise (déterministe bit-à-bit, cf. Parallélisme), pendant nodal de element_pointwise.

interp_to_gauss(field, fespace) → ElementField

Interpole un champ nodal vers les points de Gauss (valeurs, pas dérivées) :

\[ f(\xi_g) = \sum_i f_i\, N_i(\xi_g). \]

Le résultat porte les mêmes composantes que l’entrée, une valeur par (cellule, point de Gauss). C’est le pendant « valeurs » de gradient (direction nœuds → Gauss du CHAN de Cast3M) : typiquement pour porter une température nodale aux points de Gauss avant thermal_strain.

gradient(field, fespace) → ElementField

Gradient d’un champ nodal aux points de Gauss, cellule par cellule :

\[ \nabla f = \sum_i f_i\, \nabla N_i \quad \text{évalué en chaque } \xi_g. \]

Une composante de sortie <comp>_<axe> par couple (composante d’entrée, axe).

deformation(u, fespace) → ElementField

Déformation linearisée (petites déformations) d’un champ de déplacement :

\[ \varepsilon = \tfrac{1}{2}\big(\nabla u + \nabla u^\top\big). \]

u doit porter exactement space_dim composantes (déplacement selon x, y, z). Le résultat est le tenseur symétrique en convention tenseur (eps_xy = ½(∂u_x/∂y + ∂u_y/∂x), pas le cisaillement ingénieur γ), une composante eps_<ai><aj> par entrée indépendante i ≤ j. C’est l’entrée du comportement de l’élasticité.

Sur un sous-espace axisymétrique, une quatrième composante eps_zz est ajoutée : la déformation orthoradiale ε_θθ = u_r / r, que le gradient méridien ne peut pas exprimer (cf. Axisymétrie).

thermal_strain(temperature, materials, fespace, t_ref) → ElementField

Déformation thermique de libre dilatation (Cast3M EPTH), pour la thermomécanique non couplée :

\[ \varepsilon_{th} = \alpha\,(T - T_{ref})\,\big[\,1,1,(1),0,0,0\,\big]. \]

temperature est un champ par éléments portant "T" (p. ex. produit par interp_to_gauss) ; alpha est lu dans le champ matériau, où il voyage comme composante facultative de l’élasticité (à côté de E/nu, cf. Élasticité). La sortie a exactement la même disposition que deformation (composantes normales à α·ΔT, cisaillements nuls — y compris l’orthoradiale eps_zz en axisymétrique, un solide de révolution se dilatant aussi circonférentiellement), si bien que deformation(u, fespace) - thermal_strain(...) donne la déformation mécanique ε(u) − ε_th. Aucun couplage n’est fait ici : l’utilisateur compose la charge thermique et la contrainte réelle σ = D:(ε − ε_th) à partir des briques (integrate_behavior, internal_forces) — cf. l’exemple thermomécanique.

divergence(field) → NodeField

Divergence faible (consistante) d’un champ vectoriel par éléments — l’adjoint de gradient :

\[ d_i = \int_\Omega \nabla N_i \cdot F\, d\Omega \approx \sum_{\text{cell}} \sum_g (\nabla N_i \cdot F)\big|_g\, |J|_g\, w_g, \]

accumulé par nœud. C’est l’opérateur Bᵀ, transposé du gradient : il vérifie ⟨∇f, F⟩ = ⟨f, div F⟩. Le champ d’entrée doit porter exactement space_dim composantes (F_x, F_y, F_z) ; chaque sous-espace donne une zone de sortie à une composante "div". Ce sont des quantités intégrées, pas les valeurs ponctuelles de ∇·F (pas de projection L²).

beam_deformation(field, fespace, material) → ElementField

Déformations généralisées d’une poutre — Bernoulli comme Timoshenko — à partir d’un champ de déplacements et de rotations. Un opérateur pour les trois configurations, lues sur la dimension du maillage :

CoordsDDL luscomposantes produites
1-Dw, thetakappa, gamma
2-Du_x, u_y, r_zeps, kappa, gamma
3-Dsixeps, kappa_y, kappa_z, torsion, gamma_y, gamma_z

C’est le produit B · u, avec le B de l’élément (models::beam::b_into) — celui-là même dont la rigidité intègre Bᵀ D B et dont les forces internes intègrent le transposé. Les déformations sont évaluées à chaque point de Gauss : la courbure varie le long d’une travée non chargée (M' = V), seul le cisaillement est constant (V' = 0).

Le matériau est exigé, et c’est la signature honnête : Φ = 12EI/(G·A_s·L²) décide de la distribution de courbure. Un matériau sans constantes de cisaillement — celui d’une poutre de Bernoulli, qui ne demande ni G ni A_s — signifie Φ = 0, si bien que le même opérateur sert les deux théories sans qu’on ait à lui dire laquelle. Le résultat se donne au comportement pour obtenir les efforts de section.

shell_deformation(field, fespace, model) → ElementField

Déformations généralisées d’une coque, depuis les six DDL u_x…u_z, r_x…r_z. model est la formulation, "thick" ou "kirchhoff" : les lignes de membrane, de vrillage et de cisaillement sont partagées, mais celles de flexion sont toute la différence entre les deux.

formulationcomposantes produites
thickeps_xx, eps_yy, eps_xy, kappa_xx, kappa_yy, kappa_xy, drill, gamma_xz, gamma_yz
kirchhoffles mêmes, sans les deux déformations de cisaillement

Toutes dans le repère local de la facette. drill est le résidu de vrillage θ_z − ω_z, dont l’effort conjugué est un moment que la rigidité intègre comme les autres.

Le cisaillement transverse est constant par élément, échantillonné au point réduit — le point même où la rigidité l’intègre. Ce n’est pas une simplification : c’est l’intégration réduite qui empêche une coque mince de bloquer, lue de l’autre côté. L’échantillonner ailleurs rapporterait une déformation que l’élément ne porte pas.

Maths élément par élément

Onze fonctions appliquent une fonction scalaire à chaque valeur d’un champ et renvoient un nouveau champ du même type (style numpy). Elles acceptent indifféremment les quatre saveurs de champ — NodeField, SubNodeField, ElementField, SubElementField — et dispatchent par type.

PythonEffet
abs(field)valeur absolue
sqrt(field)racine carrée (nan pour les négatifs)
exp(field)exponentielle eˣ
log(field)logarithme népérien (-inf/nan pour ≤ 0)
log10(field)logarithme base 10
cos(field) / sin(field) / tan(field)trigonométrie (radians)
sinh(field) / cosh(field) / tanh(field)trigonométrie hyperbolique

Les résultats sont non bornés, comme en numpy : aucune protection sur le domaine (log de ≤ 0 donne -inf/nan, sqrt d’un négatif donne nan). Ces fonctions se combinent à l’arithmétique scalaire des champs (f + s, f * s, cf. Champ) pour bâtir des expressions par composante.

# Atténuation exponentielle d'un champ de température.
attenue = pyrucast.field.exp(temperature * -0.1)

# Magnitude of a field (combined with scalar field arithmetic).
amplitude = pyrucast.field.abs(signal)

Réduction

Deux produits scalaires, à ne pas confondre — ils diffèrent par ce qui est réduit et donc par le type du résultat :

PythonCast3MRéduitRésultat
xty(x, y)XTYtout (nœuds/points × composantes)un float
psca(x, y)PSCAles composantes seules, nœud par nœudun champ à une composante "psca"

Les deux exigent la même saveur d’opérandes (NodeField, SubNodeField, ElementField, SubElementField), alignent les composantes par nom (l’ordre peut différer) et suivent la même règle d’union que l’arithmétique de Champ : la somme ne porte que sur les (support, composante) partagés par les deux champs ; un support ou une composante d’un seul côté n’a pas de vis-à-vis et ne contribue pas.

  • xty (produit scalaire global, dot / dot_field) → un float ;
  • psca (pscal / pscal_field) → un champ (composante "psca"), une zone par support partagé.

xty(x, y) → float

Produit scalaire global des deux champs entiers :

\[ x \cdot y = \sum_i \sum_c x_{i,c}\, y_{i,c}, \]

la somme parcourant toutes les valeurs. Le résultat est un unique float : le produit scalaire qui sert au calcul d’énergie (F·u), aux normes de résidu, etc. L’addition flottante n’étant pas associative, le total dépend du nombre de threads jusqu’au dernier ULP — comme le solveur, ce n’est pas reproductible bit à bit.

# External strain energy: work of the nodal forces in the displacement field
# (same components, same mesh).
energie = pyrucast.measure.xty(forces, deplacements)

psca(x, y) → champ (même saveur que les entrées)

Produit scalaire nœud par nœud (ou point par point) — réduction sur les composantes seules, le support est conservé :

\[ p_i = \sum_c x_{i,c}\, y_{i,c}. \]

Le résultat est un nouveau champ de la même saveur que les entrées, portant une seule composante "psca" : la valeur du produit scalaire à chaque nœud. Chaque sortie est écrite une fois (par nœud) ⇒ indépendant du nombre de threads.

# Squared norm of a vector field, node by node.
norme2 = pyrucast.field.psca(vitesse, vitesse)  # one-component field, "psca"

integral(field, component, fespace=None) → float

Intègre un champ sur son support par la quadrature éléments finis, ∫_Ω f dΩ — le total d’une composante (p.ex. la résultante d’une densité de force distribuée) :

\[ \int_\Omega f \, d\Omega \;=\; \sum_{\text{cell}} \sum_g f(\text{cell}, g)\, |J|_g\, w_g . \]

  • sur un NodeField : les valeurs nodales sont relevées aux points de Gauss par les fonctions de forme, ∫ Σ_i f_i N_i dΩ — fespace est requis ;
  • sur un ElementField : les valeurs (déjà aux points de Gauss) sont intégrées directement — fespace est ignoré.

Comme xty, la somme flottante dépend du nombre de threads jusqu’au dernier ULP. En interne, la réduction parallèle sur les cellules passe par le driver kernel::reduce_cells.

# Resultant of a surface force density f_y on a plate (through N_i).
r_y = pyrucast.measure.integral(densite, "f_y", fespace=fes)
# Measure of the domain: ∫ 1 dΩ.
aire = pyrucast.measure.integral(champ_unite, "u", fespace=fes)

Somme et xtx

Pour une résultante de forces déjà nodales (sortie de internal_forces, réactions…), la résultante est une simple somme par nœud — exposée comme méthode, à côté de min / max :

RéductionRéduitRésultat
field.min(comp) / field.max(comp)une composanteun float (exact)
field.min() / field.max()toutes les composantesun float (exact)
field.sum(comp)une composante (Σ nœuds/points)un float
xtx(field)toutes les valeurs au carré (Σ v², XTX)un float
xtx(field, components=[…])seules ces composantes au carréun float

sum et xtx regroupent la somme en parallèle : dépendantes du nombre de threads au dernier ULP (contrairement à min / max, exactes quel que soit l’ordre).

Appelées sans argument, min et max lisent le champ comme la liste plate de ses valeurs — toutes composantes confondues, et, au niveau agrégat, toutes zones confondues. C’est l’esprit de xtx, et la même mise en garde : sur un champ dont les composantes ne portent pas la même unité (sigma_xx à côté de sigma_xy), la réponse est « la plus petite valeur là-dedans », pas une grandeur physique — il faut alors nommer la composante. Pour la liste des extremums composante par composante, une compréhension suffit : {c: f.min(c) for c in f.components()}.

Par défaut xtx somme toutes les composantes. En passant components, on restreint la somme à celles-là (les autres sont ignorées) — utile pour mesurer la norme d’un résidu sur un sous-jeu de degrés de liberté. Une composante absente d’une zone y est simplement ignorée ; l’appel n’échoue que si aucune zone ne porte l’une des composantes demandées.

# Resultant of a nodal force field, component by component.
rx = forces.sum("f_x")
ry = forces.sum("f_y")
# Squared norm of the residual, for a convergence test.
r2 = pyrucast.measure.xtx(residu)
# The same norm, restricted to the translation components alone.
r2_uy = pyrucast.measure.xtx(residu, components=["f_y"])
# Extrema of a named component…
fy_max = forces.max("f_y")
# …or, without an argument, of the whole field, components pooled.
partout = forces.min()

À venir

Le module est conçu pour accueillir d’autres dérivations sur le même patron (champ, espace EF) → champ : projection L² vers les nœuds (project_to_nodes), mesures non linéaires de déformation (Green-Lagrange). Elles arriveront avec les premiers besoins.