Baignage (embedded)
Une contrainte embedded (ou « baignage ») lie le champ de chaque nœud
d’un maillage immergé à l’interpolation d’un maillage hôte au même point de
l’espace. Pour un nœud immergé p logé dans la maille hôte de fonctions de forme
Nᵢ(ξ), et pour chaque composante c :
\[ u_c(p) - \sum_i N_i(\xi_p)\, u_c(\text{hôte}_i) = g_c \qquad (g_c = 0 : \text{liaison rigide}). \]
C’est l’archétype d’une barre baignée dans un volume : les nœuds de la barre
suivent le champ de déplacement volumique, sans que les deux maillages
partagent de nœud. Comme Dirichlet et MPC, c’est une
contrainte par multiplicateurs de Lagrange : ni
matériau, ni loi de comportement ; elle ne mute jamais le Coords.
C’est aussi la réponse au cas laissé ouvert par la MPC : des coefficients qui
varient par nœud (ici les Nᵢ(ξ_p), différents à chaque nœud immergé), qu’une
model.mpc à coefficients scalaires ne sait pas exprimer de façon compacte.
Implémentation : src/models/embedded.rs ; opérateur ops::model::embedded(…).
Localisation à la construction
Les poids de couplage Nᵢ(ξ_p) sont calculés une seule fois, à la
construction, en localisant chaque nœud immergé dans le maillage hôte
(mapping iso-paramétrique inverse : un Newton sur le résidu
x − Σ Nᵢ(ξ)·Xᵢ, avec test d’appartenance au domaine de référence de la maille).
Un nœud immergé qui ne tombe dans aucune maille hôte est une erreur : le
maillage immergé doit être contenu dans l’hôte. Les deux maillages doivent
partager un même Coords (les identifiants de nœuds y sont relatifs).
Les nœuds-multiplicateurs sont mintés en interne (un par nœud immergé,
colocalisé), contrairement à Dirichlet/MPC où l’utilisateur fournit le
multiplier_mesh. On y accède après coup avec Model.multiplier_mesh().
Une relation par (nœud immergé × composante)
Toutes les composantes partagent un nœud-multiplicateur par nœud immergé, chacune portant sa propre paire de variables (toutes surchargeables) :
| rôle | nom | défaut |
|---|---|---|
| primale contrainte (colonne partagée immergé ↔ hôte) | variable | — (fournie) |
| duale cible où atterrit la réaction | target_dual | — (fournie, cf. dual_of) |
primale propre = multiplicateur λ | multiplier | lambda_<variable> |
duale propre = ligne de contrainte + slot de g | imposed_value | imposed_<variable> |
Signature complète :
model.embedded(target, immersed, host, variables, tol=None)
# variables : les primales à lier, p.ex. ["u_x", "u_y", "u_z"]
La cible est le modèle contraint : chaque variable doit être une de ses
primales — refusé sinon, en nommant ce qu’elle déclare — et la ligne duale où
atterrit la réaction s’y lit. On ne redonne donc que les noms de variables,
jamais les couples (primale, duale).
Les blocs C / Cᵀ
À l’assemblage, la contrainte contribue une paire de blocs par composante. Le
nœud immergé porte le coefficient +1, chaque nœud hôte son poids −Nᵢ, si bien
que chaque relation lit u_c(p) − Σᵢ Nᵢ·u_c(hôteᵢ) = g_c :
- bloc C :
(multiplier_node, imposed_value) × (nœud, variable)=+1sur le nœud immergé,−Nᵢsur chaque nœud hôte ; - bloc Cᵀ :
(nœud, target_dual) × (multiplier_node, multiplier), mêmes coefficients (réaction réinjectée dans la physique).
Second membre et réaction
- Le second membre
g(défaut0, la liaison rigide) s’écrit dans leNodeFieldde chargement, au slotimposed_<variable>du nœud-multiplicateur. - Le multiplicateur
lambda_<variable>au nœud-multiplicateur est la force de liaison.
Exemple : barre baignée dans un HEX8
Un cube HEX8 en conduction thermique, ses huit coins fixés à un champ linéaire
T(x) = 1 + 2x + 3y + 4z (que l’interpolation trilinéaire reproduit exactement à
l’intérieur), et un nœud immergé au cœur : sa température résolue égale
l’interpolation de l’hôte.
import pyrucast
corners = [
[0, 0, 0],
[1, 0, 0],
[1, 1, 0],
[0, 1, 0],
[0, 0, 1],
[1, 0, 1],
[1, 1, 1],
[0, 1, 1],
]
field = lambda c: 1.0 + 2.0 * c[0] + 3.0 * c[1] + 4.0 * c[2]
c = pyrucast.Coords(dim=3)
corner_nodes = [c.add_node(x) for x in corners]
host = pyrucast.Mesh(c, "HEX8")
host.unit().add_cell(corner_nodes)
fes = pyrucast.FiniteElementSpace(host)
base = pyrucast.model.heat_conduction(fes)
# Corners fixed to the linear field (Dirichlet).
corner_mesh = pyrucast.mesh.poi1_from_nodes(corner_nodes)
corner_mult = pyrucast.mesh.barycenter(corner_mesh)
dirichlet = pyrucast.model.dirichlet(base, "T", corner_mesh, corner_mult)
# Immersed node, tied to the host.
p = c.add_node([0.3, 0.6, 0.2])
bar = pyrucast.mesh.poi1_from_nodes([p])
embedded = pyrucast.model.embedded(base, bar, host, ["T"])
emb_mult = embedded.multiplier_mesh().node(0, 0, 0)
model = base | dirichlet | embedded
materials = pyrucast.element_field.material_field(model, [("k", 1.0)])
# Loading: the field's value at each corner, g = 0 (tie) at the immersed node.
rhs = dirichlet.constraint_rhs([(n, field(x)) for n, x in zip(corner_nodes, corners)])
rhs = rhs | embedded.constraint_rhs([(p, 0.0)])
solution = pyrucast.solver.solve(pyrucast.matrix.stiffness(model, materials), rhs)
assert abs(solution.value(p, "T") - field([0.3, 0.6, 0.2])) < 1e-9 # 4.2
L’exemple complet est dans examples/barre_baignee.py. La variante
vectorielle — une barre suivant les déplacements d’un volume élastique en
u_x/u_y/u_z, le cas qui motive le baignage — est dans
examples/barre_baignee_elastique.py.
Limitations actuelles
- Le maillage immergé est réduit à ses nœuds (support POI1 interne) ; on lie des nœuds à une interpolation, pas des mailles à des mailles (pas de couplage surfacique / cohésif).
- La localisation fait un balayage des mailles hôtes (rejet par boîte englobante) ; pas encore d’index spatial — coûteux pour de très gros hôtes.
- Types hôtes supportés : tous les éléments à cadre de référence (SEG, TRI, QUA,
TET, PENTA, HEX — linéaires et quadratiques). Le
POI1n’a pas d’intérieur et est ignoré comme hôte.