Champ aux points de Gauss (ElementField/SubElementField)
Un champ aux points de Gauss porte des valeurs par maille et par point de Gauss : là où un champ aux nœuds donne une valeur à chaque nœud, celui-ci en donne une à chaque point d’intégration de chaque maille. Deux mailles voisines ne partagent donc rien, et le champ peut sauter d’une maille à l’autre. C’est un agrégat de zones :
- une zone (
SubElementField) porte les valeurs d’un sous-espace d’un espace éléments finis, c’est-à-dire d’une zone de maillage munie de sa quadrature ; - un champ (
ElementField) est une liste de zones, qui n’ont pas forcément les mêmes composantes.
On y range ce qui vit aux points où se calculent les intégrales :
- les propriétés matériau (module d’Young, coefficient de Poisson, conductivité, masse volumique…) ;
- les variables internes d’une loi de comportement (déformation plastique, endommagement…), gardées d’un pas de calcul au suivant ;
- les grandeurs dérivées d’une solution (déformations, contraintes, gradients, flux…).
On en construit rarement à la main : la plupart sortent des
opérateurs (material_field,
gradient, integrate_behavior…). Les règles communes aux deux familles de
champs (composantes, statistiques, arithmétique) sont décrites dans
Champ ; cette page en donne ce qui est propre aux points de Gauss.
Créer et lire un champ
pyrucast.ElementField(fes, ["E", "nu"]) crée une zone par sous-espace de
fes, avec ces composantes, toutes les valeurs à zéro.
ElementField.with_components_per_subspace(fes, [["E", "nu"], ["k"]]) donne
une liste de composantes par sous-espace. Une liste vide, un nom en double
ou un nombre de listes qui ne correspond pas aux sous-espaces sont refusés.
On lit et on écrit dans une zone : champ[i] ou champ.unit() la rendent,
et ce sont des vues (écrire dans la zone, c’est écrire dans le champ). Un point
s’y désigne par trois indices : la maille, dans l’ordre des mailles de la
zone de maillage ; le point de Gauss dans la maille, dans l’ordre de la
quadrature ; la composante, par son nom ou par son rang. Un indice hors
bornes ou une composante inconnue lèvent une erreur.
import pyrucast
# Deux triangles et leur espace éléments finis : 3 points de Gauss par TRI3.
c = pyrucast.Coords(2)
a = c.add_node([0.0, 0.0])
b = c.add_node([1.0, 0.0])
d = c.add_node([1.0, 1.0])
e = c.add_node([0.0, 1.0])
mesh = pyrucast.Mesh(c, "TRI3")
mesh.unit().add_cell([a, b, d])
mesh.unit().add_cell([a, d, e])
fes = pyrucast.FiniteElementSpace(mesh)
# Une zone par sous-espace de fes, toutes les valeurs à zéro.
mat = pyrucast.ElementField(fes, ["E", "nu"])
z = mat.unit() # la zone, une vue : écrire dans z, c'est écrire dans mat
print(z) # SubElementField: 2 cell(s) × 3 gauss × 2 component(s) [E, nu]
# Écrire : toute la zone, une maille entière, un point.
z.set_uniform("E", 210e9)
z.set_uniform("nu", 0.3)
z.set_cell_uniform(1, "E", 70e9) # la maille 1, à tous ses points de Gauss
z[1, 2, "nu"] = 0.33 # la maille 1, son point de Gauss 2
# Lire : par nom, par indice de composante, ou un point entier.
print(z.value(0, 0, "E")) # 210000000000.0
print(z[1, 2, "nu"]) # 0.33
print(z.get(1, 0, 0)) # 70000000000.0
print(z.point_values(1, 2)) # [70000000000.0, 0.33]
print(mat.max("E"), mat.min("E")) # 210000000000.0 70000000000.0
# Une liste de composantes par sous-espace (fes n'en a qu'un).
cond = pyrucast.ElementField.with_components_per_subspace(fes, [["k"]])
print(cond.components()) # ['k']
ElementField | |
|---|---|
len(champ), champ[i], for zone in champ, unit(), | | l’interface des agrégats |
support() | le sous-espace de chaque zone, dans l’ordre, partagé |
components() | l’union des composantes des zones |
min(c), max(c), sum(c) | sur toutes les zones qui portent c |
champ["c"], filter_components([…]), rename_component(a, b) | un champ réduit à ces composantes, ou une composante renommée |
add_to_component(c, s), mul_to_component(c, s)… | une opération sur une composante, en place |
consolidate() | fond les zones posées sur un même sous-espace (voir Combiner) |
plot(…) | le tracé, voir Visualisation |
SubElementField (zone) | |
|---|---|
support() | son sous-espace, partagé |
cell_count, gauss_count, component_count | les trois dimensions de la zone |
value(maille, g, "c"), zone[maille, g, "c"] | une valeur, par nom de composante |
get(maille, g, rang) | une valeur, par rang de composante |
point_values(maille, g) | toutes les composantes d’un point, dans l’ordre |
set_value, zone[…] = v, set | écrire une valeur |
set_uniform("c", v) | la composante c à v en tout point |
set_cell_uniform(maille, "c", v) | la composante c à v en tout point d’une maille |
components(), component_index("c") | les composantes, le rang de l’une |
Les positions des points de Gauss ne sont pas dans le champ : leurs
coordonnées dans l’élément de référence et leurs poids sont sur l’espace
(fes[i].gauss_xi(g), fes[i].gauss_weight(g)), et leur position réelle se
calcule à partir des nœuds.
Composantes
Chaque zone a ses propres composantes, au moins une, chacune nommée une seule
fois. Au niveau du champ, components() rend leur union, dans l’ordre de
première apparition : une composante peut n’exister que sur certaines zones.
C’est le cas d’un champ matériau multiphysique, qui porte E et nu sur la
zone de l’élasticité et k sur celle de la thermique.
champ["E"] et champ.filter_components(["E", "nu"]) rendent un nouveau champ
réduit à ces composantes. Une zone qui n’en porte aucune disparaît ; une zone
qui n’en porte pas d’autres est partagée avec le champ d’origine, sans
copie, si bien qu’écrire dans l’une modifie l’autre ; une zone dont il faut
retirer des composantes est recopiée.
Le support : un espace éléments finis
Chaque zone est posée sur un sous-espace de l’espace éléments finis, qui fixe ses mailles (celles de sa zone de maillage) et son nombre de points de Gauss par maille (celui de sa quadrature). Ces deux dimensions sont lues à la création de la zone et ne changent plus : l’espace a scellé sa zone de maillage, aucune maille ne peut y être ajoutée. Les nœuds, eux, peuvent bouger : les valeurs du champ restent ce qu’elles sont, et les points de Gauss suivent les mailles.
Deux champs se combinent point à point seulement s’ils sont posés sur le
même espace, au sens du même objet. Deux appels à
FiniteElementSpace(mesh) sur un même maillage créent deux espaces distincts,
et leurs champs restent côte à côte au lieu de s’additionner (voir l’exemple
ci-dessous). On construit donc un espace une fois et on le passe à tous les
opérateurs ; material_field(modele, …) pose ses zones sur les sous-espaces
des sous-modèles, ceux-là mêmes.
Un champ rend aussi le sien : champ.support() donne le sous-espace de chaque
zone, dans l’ordre (champ.support()[i] est champ[i].support()), et
zone.support() celui d’une zone. C’est le même objet, pas une copie : un champ
construit dessus se combine point à point avec le premier. Deux zones posées sur
un même sous-espace le citent deux fois ; s | s le garde une fois.
Un champ reste sur sa quadrature : aucun opérateur ne transporte des valeurs
d’une quadrature à une autre. Pour obtenir un champ sur un autre espace, on
l’interpole depuis les nœuds (interp_to_gauss), ou on l’intègre maille par
maille vers un espace à un point par maille (cell_integral).
Un champ garde son espace en vie, et l’espace garde son maillage et son
Coords : qu’un script ne tienne plus de variable sur l’espace ou le maillage
ne gêne pas le champ. Il ne compte pas les nœuds pour autant ; c’est la zone de
maillage qui les compte (voir Maillage).
Combiner des champs
Les opérations suivent les règles de Champ :
- une opération par un scalaire (
champ * 1.1,champ + 2.0…) s’applique à toutes les valeurs et rend un nouveau champ ; a + b,a - b,a * b,a / b,a ** bentre deux champs combinent une composante que les deux portent sur un même sous-espace, même quand chaque côté la range dans une autre de ses zones sur ce sous-espace ; une composante ou une zone présente d’un seul côté passe telle quelle ;champ + zonene touche que les zones du champ posées sur le même sous-espace ;- entre deux zones, les sous-espaces doivent être les mêmes, sinon c’est une erreur ;
abs,sqrt,exp,cos… s’appliquent valeur par valeur, etchamp >= srend un masque de 0 et de 1 (mask).
L’union a | b compose des zones sans toucher aux valeurs. Elle garde côte
à côte deux zones posées sur le même sous-espace si leurs composantes sont
disjointes, et refuse qu’une même composante y soit portée deux fois.
consolidate() fond en une seule zone celles qui partagent un sous-espace.
Contrairement au champ aux nœuds, l’union ne les fond pas d’elle-même.
# Deux champs posés sur le même espace se combinent point à point.
k1 = pyrucast.ElementField(fes, ["k"])
k2 = pyrucast.ElementField(fes, ["k"])
k1.unit().set_uniform("k", 2.0)
k2.unit().set_uniform("k", 0.5)
somme = k1 + k2
print(len(somme), somme.unit().value(0, 0, "k")) # 1 2.5
# Un second espace, même sur le même maillage, est un autre support : les
# zones restent côte à côte, rien n'est additionné.
autre = pyrucast.ElementField(pyrucast.FiniteElementSpace(mesh), ["k"])
print(len(k1 + autre)) # 2
# L'espace d'un champ, rendu tel quel : un champ posé dessus s'y combine.
print(len(pyrucast.ElementField(k1.support(), ["k"]) + k1)) # 1
# L'union garde côte à côte deux zones aux composantes disjointes ;
# consolidate les fond en une.
tout = k1 | mat
print(len(tout), tout.components()) # 2 ['k', 'E', 'nu']
print(len(tout.consolidate())) # 1
sum(c) additionne les valeurs de tous les points de Gauss, sans poids : ce
n’est pas une intégrale. L’intégrale d’une composante sur chaque maille est
cell_integral (exemple de la section suivante).
D’où viennent ces champs, et où ils vont
Les opérateurs qui produisent un champ aux points de Gauss sont rangés
dans pyrucast.element_field (détail dans Opérateurs sur les
champs, Construction et
Comportement) :
| Famille | Opérateurs |
|---|---|
| matériau | material_field(modele, [(nom, valeur)…]), material_field_per_sub_model, sub_material_field |
| d’un champ aux nœuds | interp_to_gauss, gradient, deformation, beam_deformation, shell_deformation, strain |
| comportement | integrate_behavior, elastic_stress, elastic_strain, thermal_strain, stress_measure, rotate_frame |
| d’un champ aux points de Gauss | cell_integral, mask, consolidate |
material_field crée une zone par sous-modèle qui consomme du matériau, sur
l’espace de ce sous-modèle, et ignore les autres (Dirichlet…). Il refuse une
liste à laquelle manque une composante qu’une physique attend. Deux
sous-modèles posés sur un même sous-espace qui lisent une même composante — une
conduction et une élasticité qui lisent toutes deux rho — partagent une seule
zone, puisqu’un sous-espace porte chaque composante dans une seule zone ; avec
material_field_per_sub_model, ils doivent alors lui donner la même valeur.
Ces champs sont ensuite consommés par l’assemblage (stiffness, mass,
tangent, internal_forces, external_forces, cf.
Assemblage), ramenés aux nœuds (divergence,
sum_to_nodes), tracés (plot) ou exportés (pyrucast.export.to_arrays(…, element_fields=[…])).
# Le matériau d'un modèle : une zone par sous-modèle qui en consomme.
elasticite = pyrucast.model.elasticity(fes, "plane_stress")
modele = elasticite | pyrucast.model.heat_conduction(fes)
materiau = pyrucast.element_field.material_field(
modele, [("E", 210e9), ("nu", 0.3), ("k", 45.0)]
)
print([zone.components() for zone in materiau]) # [['E', 'nu'], ['k']]
# Une température T = x aux nœuds, portée aux points de Gauss.
T = pyrucast.NodeField(mesh, ["T"])
for n in (a, b, d, e):
T[0].set_value(n, "T", n.position()[0])
T_g = pyrucast.element_field.interp_to_gauss(T, fes)
print([T_g.unit().value(0, g, "T") for g in range(3)]) # [0.5, 1.0, 0.5]
grad = pyrucast.element_field.gradient(T, fes)
print(grad.components()) # ['grad_T_x', 'grad_T_y']
print(grad.unit().point_values(0, 0)) # [1.0, 0.0]
# Une somme sur les points de Gauss n'est pas une intégrale : l'intégrale
# par maille se pose sur un espace à un point par maille.
un = pyrucast.ElementField(fes, ["un"]) + 1.0
centres = pyrucast.FiniteElementSpace(mesh, quadrature="REDUCED")
aires = pyrucast.element_field.cell_integral(un, centres)
print(un.sum("un"), aires.sum("un")) # 6.0 1.0
Ce qui est mis en cache
Un champ aux points de Gauss ne garde aucun cache : il ne contient que ses valeurs, les noms de ses composantes et ses deux dimensions. Ce qui ne se calcule qu’une fois vit sur l’espace éléments finis qui le porte :
- les tables de référence (coordonnées et poids des points de Gauss, fonctions de forme et leurs dérivées en ces points), calculées à la création de l’espace ;
- la coloration des mailles : des groupes de mailles qui ne partagent aucun
nœud. Elle sert aux opérateurs qui accumulent des contributions de mailles
sur les nœuds (l’assemblage d’une matrice,
divergence,sum_to_nodes,internal_forces,external_forces) : les mailles d’un même groupe s’écrivent en parallèle sans conflit. Elle est calculée au premier de ces calculs, puis resert à tous les suivants sur ce sous-espace.
C’est une raison de plus de réutiliser un même espace : un second espace sur le même maillage refait ses tables et sa coloration.
Visualisation
champ.plot(component="E") trace le champ sur son propre support : chaque zone
retrouve sa zone de maillage par son sous-espace, sans copie. Le rendu reste
par maille : dans chaque maille, des valeurs aux nœuds sont ajustées aux
valeurs de Gauss par moindres carrés, sans moyenne avec les mailles voisines,
si bien que les sauts d’une maille à l’autre (contraintes, flux) restent
visibles. smooth règle la subdivision des mailles (0 : une couleur par
maille). Voir Visualisation.
Sauvegarde
pyrucast.save écrit le champ avec son espace, son maillage
et son Coords. Des champs sauvegardés dans un même fichier partagent
toujours leur espace à la relecture et se combinent point à point ; un champ
relu ne partage pas son espace avec ceux du script qui l’a écrit, ni avec ceux
d’un autre fichier. Le scellement de la zone de maillage est relu avec elle ;
la coloration, elle, n’est pas écrite et se recalcule au premier usage.
Côté Rust
SubElementField porte un Handle<SubFiniteElementSpace>, ses noms de
composantes, ses deux dimensions et un Vec<f64> à plat, rangé maille →
point de Gauss → composante :
values[maille * gauss_count * component_count
+ g * component_count
+ c]
Les composantes d’un point sont donc contiguës : point_values(maille, g) en
rend la tranche sans copie, et c’est ce que lit un noyau de calcul.
ElementField est l’agrégat de Handle<SubElementField> ; son finalize
refuse une composante portée deux fois sur un même sous-espace.
#[test]
fn un_champ_aux_points_de_gauss_porte_le_materiau() -> Result<()> {
let coords = Handle::new(Coords::new(2)?);
let a = Node::create_in(coords.clone(), &[0.0, 0.0])?;
let b = Node::create_in(coords.clone(), &[1.0, 0.0])?;
let c = Node::create_in(coords.clone(), &[0.0, 1.0])?;
let mut mesh = Mesh::from_submesh(SubMesh::new(coords, ElementType::TRI3));
mesh.add_cell(&[a.id(), b.id(), c.id()])?;
let fes = FiniteElementSpace::lagrange1(&mesh)?;
// 2-D linear elasticity: two material properties, one zone (one subspace).
let mat = ElementField::new(&fes, vec!["E".into(), "nu".into()])?;
{
let mut z = mat.get(0)?.write(); // la zone (SubElementField) — guard
z.set_uniform("E", 210e9)?; // module d'Young constant
z.set_uniform("nu", 0.3)?; // Poisson constant
assert_eq!(z.value(0, 0, "E")?, 210e9);
}
// Components per subspace (multi-material):
let mat2 = ElementField::with(
&fes,
&[vec!["E".into(), "nu".into()]], // une liste par sous-espace
)?;
// Statistics and arithmetic at the aggregate level.
assert_eq!(Field::max(&mat, Some("E"))?, 210e9);
let scaled = &mat * 1.1; // nouveau champ (référence : préserve `mat`)
mat.mul_to_component("E", 0.95)?; // en place, seulement "E"
let _ = (mat2, scaled);
Ok(())
}
- Contrat commun. Les noms de composantes,
min/max/sum, les opérations par composante et l’arithmétique viennent des traitsSubFieldetField(src/containers/field.rs), partagés avec le champ aux nœuds. - Support.
SubField::same_supportcompare lesHandlepar identité (same_object) : c’est ce qui fait de deux espaces sur un même maillage deux supports distincts. - Durée de vie. Le
Handlede l’espace est unArc: la zone le garde en vie, l’espace garde sonSubMesh, qui garde sonCoordset compte ses nœuds (cf. Modèle mémoire). - Coloration. Un
OnceLocksurSubFiniteElementSpace, rempli parcoloring(…)et marqué#[serde(skip)].