Contact (nœud-surface)
En bref
Empêche les nœuds d’un maillage esclave de pénétrer une surface maître, sans frottement : deux blocs posés l’un sur l’autre, une pièce contre un appui.
| opérateur | pyrucast.model.contact(slave, master, target, variables) |
| tag | constraint et celui de la cible |
| type | contrainte (label() : "Contact") |
| support | les nœuds esclaves, et les facettes maîtres : SEG2, SEG3 en 2-D ; TRI3, QUA4, TRI6, QUA8, QUA9 en 3-D |
variables sont les déplacements de la cible, un par dimension de l’espace,
dans l’ordre (["u_x", "u_y"]). Les deux maillages partagent un même
Coords. La surface maître doit être orientée vers le corps esclave : en
2-D, la normale d’un segment est sa tangente tournée de −90°,
\(n = (t_y, -t_x)\) ; en 3-D, elle suit la règle de la main droite sur
l’ordre des nœuds.
Théorie
Chaque nœud esclave \(s\) est apparié à son point le plus proche sur la surface maître : une facette, des poids \(N_i(\xi)\), une normale \(n\) et un jeu signé \(g_0\), positif quand c’est décollé. La non-pénétration linéarisée est une relation unilatérale par nœud esclave :
\[ g_0 + n\cdot u(s) - \sum_i N_i(\xi)\,n\cdot u(\text{maître}_i) \ \ge\ 0 . \]
Une seule relation scalaire par nœud esclave : la normale couple les
composantes, avec les coefficients \(+n_c\) sur l’esclave et
\(-N_i\,n_c\) sur chaque nœud maître. solve y trouve des inégalités et lance
l’ensemble actif (voir Contraintes) :
le multiplicateur vaut \(\lambda \le 0\) tant que la paire touche, et
exactement 0 quand elle est décollée : la réaction de contact, \(-C^{\mathsf
T}\lambda\), ne fait que pousser.
L’appariement est calculé à la construction, par projection au point le
plus proche (ops::geom::project_points), \(\xi\) ramené au domaine de
référence : un nœud face à un bord se projette sur le bord. Il est ensuite
figé : petits déplacements, petits glissements.
Une surface maître courbe reste courbe. La projection se fait sur la facette elle-même, avec ses propres fonctions de forme : une facette
TRI6,QUA8ouQUA9est courbe, et le point apparié comme sa normale sont pris sur cette surface courbe. Pour une pièce maillée en éléments quadratiques, on donne donc comme surface maître les faces du maillage telles quelles : il ne faut pas les découper en triangles platsTRI3.Un triangle plat tendu entre deux nœuds d’un bord courbe en suit la corde, et la corde s’écarte du vrai bord. Dans un alésage, elle passe à l’intérieur du trou : un nœud esclave posé sur le vrai bord se retrouve derrière la facette, compté comme déjà enfoncé, et le solveur le repousse. L’axe entre alors en force dans un trou devenu trop petit, et les deux pièces sont chargées avant que le chargement commence. Sur une surface bombée vers l’esclave, c’est l’inverse : un jeu qui n’existe pas, et un contact qui se ferme trop tard.
L’écart est la flèche de la corde, \(r\,(1 - \cos(\Delta\theta/2))\) pour un bord de rayon \(r\) dont la facette couvre l’angle \(\Delta\theta\). Raffiner le maillage le réduit vite, puisqu’il se divise par quatre quand la taille des mailles est divisée par deux ; mais un contact se joue sur de petits déplacements, et cet écart s’y ajoute tel quel. Une face quadratique passe aussi par les nœuds milieux et se courbe entre eux : elle reste bien plus près du vrai bord. Dans un maillage linéaire, les faces
TRI3etQUA4sont plates de toute façon : la flèche est là, et seul le raffinement du bord courbe la réduit.
Le jeu est un chargement que porte le contact. Le second membre de chaque
relation est \(-g_0\), une donnée géométrique : external_forces le rend, à
la composante imposed de chaque nœud multiplicateur, avec les autres
chargements du modèle.
Grands glissements. update_contact(modele, deplacement) (ou
modele.update_contact(deplacement)) rend le modèle avec ses contacts
appariés de nouveau sur les positions \(X + u\), \(X\) étant la
configuration active ; les autres sous-modèles sont partagés tels quels. Les
nœuds multiplicateurs ne changent pas ; les coefficients et le second membre,
si. Le jeu rangé est celui du nouvel appariement mesuré sur \(X\),
\[ \tilde g = n\cdot\Big(X_s - \textstyle\sum_i N_i\,X_{m_i}\Big), \]
si bien que \(\tilde g + C\,u\) est le jeu réel en \(X + u_k\), linéarisé
là. Pour une projection au point le plus proche, la variation du jeu se réduit
à \(n\cdot(\delta x_s - \sum N_i\,\delta x_{m_i})\) ; seul le terme du second
ordre, la raideur géométrique du contact, est négligé. update_contact(modele, 0)
redonne l’appariement de construction. Le pas à pas
met ses contacts à jour chaque fois qu’il reconstruit son opérateur.
Variables
| primale | lambda, le multiplicateur de contact (la réaction est \(-C^{\mathsf T}\lambda\)) |
| duale | imposed |
| ce qu’il contraint | les déplacements variables, aux nœuds esclaves et maîtres |
| matériau, comportement | aucun |
Ce qu’il produit
| raideur | une paire \(C\), \(C^{\mathsf T}\) |
| masse, géométrique | — |
| tangente | = raideur |
| forces internes | \(C^{\mathsf T}\lambda\) sur les duales, \(C\,u\) sur les lignes imposed |
| forces externes | \(-g_0\) sur les lignes imposed |
Exemple
Deux blocs élastiques empilés, u_x bloqué partout, une pression S sur le
bloc du haut : le contact se ferme et transmet \(\sigma_{yy} = -S\), les
réactions \(-\lambda_i\) étant les forces nodales de la pression.
# Maître : le bord haut du bloc du bas, parcouru en −x (normale +y, vers l'esclave).
master = pyrucast.Mesh(c, "SEG2")
for i in reversed(range(N)):
master.unit().add_cell([bottom[idx(i + 1, N)], bottom[idx(i, N)]])
# Esclave : les nœuds du bord bas du bloc du haut.
slave = pyrucast.mesh.poi1_from_nodes([top[idx(i, 0)] for i in range(N + 1)])
contact = pyrucast.model.contact(slave, master, elasticite, ["u_x", "u_y"])
# La pression sur le bord haut est un terme du modèle, comme le contact.
charge = pyrucast.model.flux(edge_fes, elasticite, "f_y")
model = elasticite | appuis | contact | charge
materials = pyrucast.element_field.material_field(
model, [("E", 210.0), ("nu", 0.0), ("phi_f_y", -S)]
)
# La pression, et le jeu que porte le contact.
rhs = pyrucast.node_field.external_forces(model, materials)
solution = pyrucast.solver.solve(pyrucast.matrix.stiffness(model, materials), rhs)
Le jeu que porte le contact, ici nul :
# Le jeu est un chargement que porte le contact : seul, `external_forces` rend
# −g0 à chacun de ses nœuds multiplicateurs ; ici 0, les blocs se touchant.
gaps = pyrucast.node_field.external_forces(contact, materials)
mult_mesh = contact.multiplier_mesh()
for r in range(N + 1):
assert gaps.value(mult_mesh.node(0, r, 0), "imposed") == 0.0
Le bloc du haut glissé d’un quart, et les contacts appariés de nouveau :
# Le bloc du haut a glissé d'un quart vers la droite : chaque nœud esclave est
# apparié de nouveau à sa position déplacée ; les multiplicateurs restent les mêmes.
glisse = pyrucast.NodeField(pyrucast.mesh.poi1_from_nodes(top), ["u_x", "u_y"])
for n in top:
glisse[0].set_value(n, "u_x", 0.25)
nouveau = contact.update_contact(glisse) # ou pyrucast.model.update_contact(…)
assert nouveau.multiplier_mesh().node(0, 0, 0).id == mult_mesh.node(0, 0, 0).id
Côté Rust
Contact (src/models/contact.rs) implémente Constraint et déclare son
jeu comme force externe. tests/contact.rs et tests/python/test_contact.py
déroulent les deux blocs en 2-D et en 3-D, et vérifient que soulever le bloc
du haut relâche toutes les paires.