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

Coordonnées (Coords)

Un Coords est le magasin des nœuds : il leur donne une identité et des coordonnées, en dimension fixée, dans une ou plusieurs configurations (référence, déformée…). C’est le premier objet de pyrucast — maillages, champs et espaces éléments finis s’y rattachent. On y crée les nœuds, et chacun se manipule par un Node, l’objet du chapitre suivant.

import pyrucast

c = pyrucast.Coords(dim=2)
a = c.add_node([0.0, 0.0])  # un pyrucast.Node
b = c.add_node([1.0, 0.0])
print(a.id, b.id)  # 0 1 — identifiants stables
print(c)  # Coords: dim=2, configs=1 (active="default"), nodes=2 …

ligne = pyrucast.mesh.line(a, b, 2)  # pose un nœud au milieu, id 2
print(c.refcount(a.id))  # 2 — la variable a, et la maille qui le contient
print(c.refcount(2))  # 2 — les deux mailles qui le contiennent
milieu = c.acquire(2)  # un Node pour un id existant
print(milieu.position())  # [0.5, 0.0]

Identité et durée de vie des nœuds

Chaque nœud créé reçoit un identifiant (node.id) stable et jamais réutilisé, valable pour toute la vie du Coords. C’est par lui que les maillages et les champs désignent leurs nœuds.

Un nœud ne se supprime pas : il n’existe aucune méthode pour le retirer, et ses coordonnées restent lisibles tant que le Coords existe. refcount(id) dit combien de références le désignent — chaque Node qui le tient, chaque maille qui le contient — et acquire(id) rend un nouveau Node pour un identifiant existant. Ces deux méthodes prennent un identifiant brut, pas un Node : passer un Node ajouterait la référence même qu’on cherche à compter.

Repère de révolution

Un Coords déclare aussi comment lire ses coordonnées : cartésien par défaut, ou axisymétrique — le plan méridien \( (r, z) \) d’un solide de révolution, avec \( x = r \ge 0 \) (rayon) et \( y = z \) (axe). C’est l’équivalent du OPTI MODE AXIS de Cast3M, et sa place est bien la géométrie : le repère change la mesure d’intégration elle-même,

\[ d\Omega = 2\pi r \, |J| \, d\xi, \]

donc rigidité, masse, conductivité, flux réparti, volumes et forces internes d’un coup — sur le corps comme sur ses bords. Le facteur \( 2\pi \) est celui de l’anneau complet : les masses, volumes et résultantes nodales sont ceux de la pièce de révolution entière.

import pyrucast

c = pyrucast.Coords.axisymmetric()
assert c.dim == 2 and c.is_axisymmetric
c.add_node([1.0, 0.0])  # r = 1, z = 0
try:
    c.add_node([-1.0, 0.0])  # x is a radius: it must be ≥ 0
except RuntimeError as erreur:
    print(erreur)

Un rayon négatif est refusé par add_node comme par set_position, plutôt que de ressortir en |J| négatif au fond d’une intégrale. Tout espace éléments finis bâti sur ces Coords hérite du repère, si bien qu’un corps et son bord ne peuvent pas diverger.

Côté mécanique, l’axisymétrie ajoute la déformation orthoradiale \( \varepsilon_{\theta\theta} = u_r/r \), qui relève du modèle et non de la géométrie : voir Élasticité linéaire. La thermique n’a rien à changer.

Côté visualisation, un tracé axisymétrique montre par défaut la section méridienne ; l’option revolve de plot la balaie pour dessiner le corps de révolution lui-même — voir Visualisation.

Plusieurs configurations

Une configuration est un jeu complet de coordonnées pour les mêmes nœuds : référence, déformée, prédite. add_config(nom) clone la configuration active sous un nouveau nom et rend son indice ; select(i) change la configuration active, et node.position() lit toujours dans celle-là.

import pyrucast

c = pyrucast.Coords(dim=2)
n = c.add_node([0.0, 0.0])

# Create a second configuration (a clone of the active one).
c2 = c.add_config("deformed")
print(c.names())  # ['default', 'deformed']

# Switch to the deformed configuration and change the coordinates.
c.select(c2)
n.set_position([0.1, 0.05])

# The coordinates read depend on the active configuration.
c.select(0)
print(n.position())  # [0.0, 0.0]  — configuration de référence
c.select(c2)
print(n.position())  # [0.1, 0.05] — configuration déformée
print(c.active)  # 1

Pour calculer dans une autre configuration — une matrice sur la déformée, une normale sur la paroi telle qu’elle est —, with coords.in_config(i): l’active le temps du bloc, puis rend la configuration d’avant, même si le bloc lève une exception. C’est la forme à préférer à une paire select(i) … select(0), qui laisse la mauvaise configuration active à la première exception. Les blocs s’imbriquent. La configuration active appartient au Coords, pas au bloc : un autre fil qui calcule sur le même Coords pendant le bloc la voit aussi.

import pyrucast

c = pyrucast.Coords(dim=2)
n = c.add_node([1.0, 0.0])
deformee = c.add_config("deformed")

# Le bloc voit `deformee` ; la configuration d'avant revient à la sortie,
# même après une exception.
with c.in_config(deformee):
    n.set_position([1.5, 0.0])
    x = n.position()

print(x)  # [1.5, 0.0]
print(n.position())  # [1.0, 0.0] — la référence, de nouveau active

Permutation pour le solveur

Une permutation optionnelle fixe l’ordre des nœuds pour le solveur sans toucher à leur identité : permutation[id] est la place du nœud id, et les matrices assemblées rangent leurs degrés de liberté dans cet ordre. Elle se pose à la main par set_permutation, avec une entrée par nœud créé (capacity) et aucune valeur en double ; clear_permutation revient à l’ordre des identifiants.

import pyrucast

c = pyrucast.Coords(dim=2)
c.add_node([0.0, 0.0])
c.add_node([1.0, 0.0])
c.add_node([0.5, 1.0])

# One entry per node created, no value twice.
print(c.capacity)  # 3
c.set_permutation([2, 0, 1])
print(c.permutation())  # [2, 0, 1]

# Back to the identity (None = identity).
c.clear_permutation()
print(c.permutation())  # None

Inspecter un Coords

dim, is_axisymmetric, names() et active décrivent le Coords ; print(c) le résume en une ligne, repr(c) en donne la structure et c.dump() la table complète des nœuds, coordonnées et références — les trois niveaux de listing.

Pourquoi plusieurs configurations dans un seul Coords

L’autre conception possible serait un Coords par configuration. pyrucast garde un seul jeu d’identités par domaine, et plusieurs configurations dedans :

Plusieurs configurations dans un Coords (pyrucast)Un Coords par configuration
identitél’id 42 désigne le même nœud dans toutes les configurations ; maillages et champs restent valides quelle que soit la configuration activel’id 42 de deux objets désigne deux nœuds différents : tout maillage ou champ à cheval doit porter une table de correspondance
changer de configurationun changement d’indice, rien à renuméroterun autre objet, et ce qui le référence à reconstruire
limitetoutes les configurations ont les mêmes nœudsdes jeux de tailles différentes sont possibles

Des Coords distincts servent aux domaines vraiment indépendants.

Côté Rust

En Rust, un Coords se partage par un Handle<Coords>, une référence comptée munie de son verrou, et chaque nœud porte en plus son propre compteur dans le Coords : c’est lui que refcount lit, et que Node, les maillages et les champs incrémentent. Le détail est dans Modèle mémoire. Les mêmes opérations s’écrivent ainsi :

use pyrucast::coords::Coords;

#[test]
fn le_repere_se_choisit_a_la_construction() {
    // Cartesian (the default): free dim.
    let plan = Coords::new(2).unwrap();
    assert!(!plan.is_axisymmetric());

    // Revolution: the dimension is necessarily 2, hence no argument.
    let axi = Coords::axisymmetric();
    assert_eq!(axi.dim(), 2);
    assert!(axi.is_axisymmetric());
}
#[test]
fn une_seconde_configuration_clone_la_courante() {
    let coords = Handle::new(Coords::new(2).unwrap());
    let id = coords.write().add_node(&[0.0, 0.0]).unwrap();

    let c2 = coords.write().add_config("deformed");
    coords.write().select(c2).unwrap();
    // the following `set_position` now change the "deformed" configuration.
    coords.write().set_position(id, &[0.1, 0.05]).unwrap();

    coords.write().select(0).unwrap();
    assert_eq!(coords.read().position(id).unwrap(), vec![0.0, 0.0]);
    coords.write().select(c2).unwrap();
    assert_eq!(coords.read().position(id).unwrap(), vec![0.1, 0.05]);
}

in_config y reçoit le bloc en fermeture, l’idiome Rust du with Python :

#[test]
fn calculer_dans_une_autre_configuration() -> pyrucast::Result<()> {
    use pyrucast::atoms::Node;
    use pyrucast::handle::Handle;
    use pyrucast::ops::coords as ops_coords;

    let coords = Handle::new(Coords::new(2)?);
    let n = Node::create_in(coords.clone(), &[1.0, 0.0])?;
    let deformee = coords.write().add_config("deformed");

    // Le bloc voit `deformee` ; la configuration d'avant revient à la sortie,
    // même si le bloc échoue ou panique.
    ops_coords::in_config(&coords, deformee, || n.set_position(&[1.5, 0.0]))?;
    let x = ops_coords::in_config(&coords, deformee, || n.position())?;

    assert_eq!(x, vec![1.5, 0.0]);
    assert_eq!(n.position()?, vec![1.0, 0.0]); // la référence, de nouveau active
    Ok(())
}
#[test]
fn une_permutation_renumerote_pour_le_solveur() {
    let coords = Handle::new(Coords::new(2).unwrap());
    // Trois nœuds créés ; ids = 0, 1, 2.
    coords.write().add_node(&[0.0, 0.0]).unwrap();
    coords.write().add_node(&[1.0, 0.0]).unwrap();
    coords.write().add_node(&[0.5, 1.0]).unwrap();

    // Permutation set by hand: one entry per node created.
    coords.write().set_permutation(vec![2, 0, 1]).unwrap();
    // permutation[0] = 2: the node with id 0 is at solver position 2.
    println!("{:?}", coords.read().permutation());

    // Back to the identity.
    coords.write().clear_permutation();
    assert!(coords.read().permutation().is_none());
}