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 active | l’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 configuration | un changement d’indice, rien à renuméroter | un autre objet, et ce qui le référence à reconstruire |
| limite | toutes les configurations ont les mêmes nœuds | des 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());
}