Barre
En bref
Une barre ne transmet que l’effort axial : un treillis, un câble tendu, un tirant. Elle se pose sur des segments, en 1-D, 2-D ou 3-D.
| opérateur | pyrucast.model.truss(fespace, strain_measure="linear") |
| tag | mechanical |
| type | domaine avec comportement (label() : "Truss") |
| éléments | SEG2 |
Une barre n’a aucune raideur transversale : chaque nœud doit être tenu dans
les directions que la barre ne porte pas (par d’autres barres, un treillis
triangulé, ou des appuis), sinon la matrice est singulière. Tout autre
élément qu’un SEG2 est refusé, comme une géométrie de révolution.
Théorie
Le long de l’axe \(s\) de la barre, de section \(A\) :
\[ N = E\,A\,\varepsilon, \qquad \varepsilon = \frac{du_s}{ds}, \qquad \frac{dN}{ds} + f = 0 . \]
La direction de la barre est le vecteur unitaire \(c = (x_B - x_A)/L\), tiré des coordonnées des nœuds. Avec l’interpolation linéaire, la déformation est constante dans l’élément, et la raideur s’écrit directement dans les axes globaux, sans matrice de repère :
\[ K_e = \frac{E\,A}{L} \begin{bmatrix} c\,c^{\mathsf T} & -c\,c^{\mathsf T} \\ -c\,c^{\mathsf T} & c\,c^{\mathsf T} \end{bmatrix}. \]
En 1-D, \(c = 1\) et l’on retrouve \((EA/L)\,[[1, -1], [-1, 1]]\). La masse est \((\rho A L/6)\,[[2, 1], [1, 2]]\) sur chaque direction, et la raideur géométrique sous l’effort \(N\) vaut \((N/L)\,[[I, -I], [-I, I]]\) : elle raidit toutes les directions, l’axiale comprise.
Avec strain_measure="green_lagrange", la barre suit les grands
déplacements : voir Grandes transformations.
Variables
Selon la dimension des Coords :
| 1-D | 2-D | 3-D | |
|---|---|---|---|
| primales | u_x | u_x, u_y | u_x, u_y, u_z |
| duales | f_x | f_x, f_y | f_x, f_y, f_z |
| entrée du comportement | eps_xx | eps_xx, eps_yy, eps_xy | eps_xx, eps_yy, eps_zz, eps_yz, eps_xz, eps_xy |
| sortie du comportement | n | n | n |
n est l’effort axial \(E\,A\,c^{\mathsf T}\varepsilon\,c\), lu le long de la
barre dans la déformation du point.
| matériau requis | E, A |
| matériau facultatif | rho |
| état | aucun |
Ce qu’elle produit
| raideur | \(K_e\) |
| masse | exige rho |
| géométrique | sous l’effort n |
| tangente | = raideur |
| forces internes | \(n\,c\) aux deux nœuds, de signes opposés : le même \(B\) que la raideur |
Exemple
Une barre de longueur 2, de module 200 000 et de section 0,01, tirée par une force de 1000 : \(u = F\,L/(E\,A) = 1\).
import pyrucast
# Une barre de longueur 2, fixée à gauche, guidée en y à droite, tirée par F = 1000.
c = pyrucast.Coords(2)
a = c.add_node([0.0, 0.0])
b = c.add_node([2.0, 0.0])
ligne = pyrucast.mesh.line(a, b, 1)
barre = pyrucast.model.truss(pyrucast.FiniteElementSpace(ligne))
appuis = (
pyrucast.model.dirichlet(pyrucast.mesh.poi1_from_nodes([a]), barre, "u_x")
| pyrucast.model.dirichlet(pyrucast.mesh.poi1_from_nodes([a]), barre, "u_y")
| pyrucast.model.dirichlet(pyrucast.mesh.poi1_from_nodes([b]), barre, "u_y")
)
modele = barre | appuis
materiaux = pyrucast.element_field.material_field(modele, [("E", 200e3), ("A", 0.01)])
force = pyrucast.NodeField(ligne, ["f_x"])
force.unit().set_value(b, "f_x", 1000.0)
u = pyrucast.matrix.stiffness(modele, materiaux).solve(force) # valeurs imposées nulles
print(u.value(b, "u_x")) # 1.0 = F·L/(E·A)
deformation = pyrucast.element_field.strain(modele, u, materiaux)
repos = pyrucast.element_field.initial_state(
modele, materiaux
) # l'état de départ de la loi
effort = pyrucast.element_field.integrate_behavior(
modele, deformation, materiaux, repos
)
print(effort.components(), effort[0].value(0, 0, "n")) # ['n'] 1000.0
Côté Rust
Truss (src/models/truss.rs) calcule ses matrices par des fonctions pures,
element_stiffness, element_mass et element_geometric, et ses forces
internes par internal_force_element, qui applique le même \(B\) que la
raideur : pour une loi linéaire, \(\int B^{\mathsf T}\sigma\) vaut exactement
\(K\,u\).
use pyrucast::aggregate::Aggregate;
use pyrucast::atoms::{ElementType, Node};
use pyrucast::containers::finite_element_space::FiniteElementSpace;
use pyrucast::containers::mesh::{Mesh, SubMesh};
use pyrucast::containers::model::Model;
use pyrucast::containers::node_field::{NodeField, SubNodeField};
use pyrucast::coords::Coords;
use pyrucast::handle::Handle;
use pyrucast::ops::element_field::StrainMeasure;
use pyrucast::ops::model;
use pyrucast::ops::solver::solve;
use pyrucast::Result;
#[test]
fn truss_bar_recovers_axial_elongation() -> Result<()> {
const E: f64 = 210.0e9; // Young's modulus (Pa)
const A: f64 = 1.0e-4; // section area (m²)
const L: f64 = 2.0; // length (m)
const F: f64 = 1000.0; // axial force at the right end (N)
// ── Mesh: one horizontal SEG2 bar ──────────────────────────────────────
let coords = Handle::new(Coords::new(2)?);
let n0 = Node::create_in(coords.clone(), &[0.0, 0.0])?;
let n1 = Node::create_in(coords.clone(), &[L, 0.0])?;
let mut mesh = Mesh::from_submesh(SubMesh::new(coords.clone(), ElementType::SEG2));
mesh.add_cell(&[n0.id(), n1.id()])?;
let fes = FiniteElementSpace::lagrange1(&mesh)?;
// ── Modèle : barre + appuis (Dirichlet homogènes) ──────────────────────
// Homogeneous (u = 0) BCs: the imposed value defaults to 0, so we only need
// to introduce the constraint. A bar has no transverse stiffness, hence
// `u_y` is clamped at both nodes to make the system well-posed.
let clamp = |target: &Model, node: &Node, var: &str| -> Result<Model> {
let imposed = Mesh::from_submesh(SubMesh::poi1_from_nodes(std::slice::from_ref(node))?);
model::dirichlet(&imposed, target, var, Default::default())
};
let mut model = model::truss(&fes, StrainMeasure::Linear)?;
model = model.union(&clamp(&model, &n0, "u_x")?)?;
model = model.union(&clamp(&model, &n0, "u_y")?)?;
model = model.union(&clamp(&model, &n1, "u_y")?)?;
// ── Matériau E, A (Dirichlet ignoré automatiquement) ───────────────────
let materials = pyrucast::ops::element_field::material_field(&model, &[("E", E), ("A", A)])?;
// ── Loading: axial force F at the right node ───────────────────────────
let mut load_sm = SubMesh::new(coords.clone(), ElementType::POI1);
load_sm.add_cell(&[n1.id()])?;
let load_sm = Handle::new(load_sm);
let mut rhs = SubNodeField::from_poi1(&load_sm, vec!["f_x".into()])?;
rhs.set_value(n1.id(), "f_x", F)?;
let rhs = NodeField::from_sub(rhs);
// ── Assemblage + résolution ────────────────────────────────────────────
let stiffness = pyrucast::ops::matrix::stiffness(&model, &materials)?;
let solution = solve(&stiffness, &rhs)?;
// ── Comparaison à l'analytique : u_x = F·L / (E·A) ─────────────────────
let expected = F * L / (E * A);
let ux = solution.value(n1.id(), "u_x")?;
assert!(
(ux - expected).abs() < 1e-10 * expected,
"u_x = {ux}, attendu {expected}"
);
// The left node is clamped, the right end does not move transversally.
assert!(solution.value(n0.id(), "u_x")?.abs() < 1e-18);
assert!(solution.value(n1.id(), "u_y")?.abs() < 1e-18);
Ok(())
}