Coordonnées (Coords)
Un Coords héberge une ou plusieurs configurations (jeux de coordonnées)
pour le même ensemble de nœuds, en dimension fixée. C’est le premier objet du
modèle pyrucast — tous les autres (Mesh, NodeField, FE space…) viennent s’y
greffer. L’accesseur utilisateur d’un nœud, le Node, fait
l’objet du chapitre suivant.
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.
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().unwrap();
assert_eq!(axi.dim(), 2);
assert!(axi.is_axisymmetric());
}
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é à l’ajout (et au 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 la balaie pour dessiner le corps de révolution
lui-même — voir
Visualisation.
Identité d’un nœud
Chaque nœud créé reçoit un identifiant interne stable (NodeId), unique
pour toute la vie du Coords : aucun id n’est jamais réutilisé, même après
ramassage par le GC. C’est ce qui permet aux maillages et champs de référencer
un nœud par son id sans s’inquiéter de la stabilité.
Politique de suppression : pas de suppression directe
Il n’existe aucune méthode remove_node. Un nœud référencé est protégé.
Seul le ramasse-miettes Coords::gc() retire les nœuds dont le refcount
interne est tombé à 0.
use pyrucast::handle::Handle;
#[test]
fn un_noeud_survit_tant_qu_on_le_tient() {
let coords = Handle::new(Coords::new(2).unwrap());
// add_node initializes refcount = 1; without a decrement, the node is protected.
let id = coords.write().add_node(&[0.0, 0.0]).unwrap();
assert_eq!(coords.write().gc(), 0);
// Après décrément, gc ramasse.
coords.write().decref(id).unwrap();
assert_eq!(coords.write().gc(), 1);
}
Modèle de refcount à deux niveaux
Handle<Coords> ◀── refcount de l'Arc
│ (le Coords est-il vivant ?)
▼
┌──────────────────┐
│ Coords │
└──────────────────┘
│
│ refcount par nœud
▼ ◀── refcount sur chaque NodeId interne
NodeId(0) NodeId(1) … (le nœud est-il vivant ?)
Les deux niveaux sont indépendants :
- tant qu’un
Handle<Coords>existe, leCoordsreste en mémoire ; - tant qu’au moins un
Node(ou un objet aval comme Mesh / Field viaincref/decref) référence unNodeId, ce nœud est protégé du GC.
Le détail du niveau objet est dans Modèle mémoire ; le niveau nœud, dans Nœud.
Plusieurs configurations
Utile pour basculer entre référence / déformée / prédite. La configuration
active est désignée par index ; lire les coordonnées d’un nœud (node.position()
en Python, Coords::position côté Rust) renvoie celles de la configuration
active. add_config(name) clone la configuration active sous un nouveau nom.
Rust :
#[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]);
}
Python :
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
Pourquoi plusieurs configurations dans un Coords plutôt que plusieurs Coords ?
cast3m suit historiquement la convention inverse : un objet de coordonnées = une seule configuration, et on multiplie les objets. Les deux modèles ont des compromis distincts.
| Modèle | Avantages | Limites |
|---|---|---|
Plusieurs configurations dans un Coords (pyrucast actuel) | NodeId 42 désigne le même nœud physique dans toutes les configurations. Les maillages et champs restent valides quelle que soit la configuration active (référence / déformée / prédite). Topologie, refcount, permutation mutualisés. select(config) est un simple changement d’index — pas de remapping aval. | Toutes les configurations ont le même cardinal de nœuds. Pas de configuration “partielle” ne couvrant qu’une portion du domaine. |
Un Coords par configuration (modèle cast3m historique) | Chaque objet est autonome : sérialisation et GC indépendants. Permet des jeux de tailles différentes (sous-problèmes, maillages adaptés). | NodeId 42 dans coords_A ≠ NodeId 42 dans coords_B : tout maillage ou champ référençant plusieurs Coords doit porter une table de correspondance explicite. Source classique de bugs (« nœud copié au lieu de partagé »). Ajouter un nœud « à tous les Coords équivalents » est une opération transverse non triviale. |
Rien n’énumère les Coords vivants — chacun n’est atteignable que par les
handles qui le désignent (cf. Modèle mémoire) : propager un
add_node « à tous les Coords équivalents » n’aurait de toute façon aucun
point d’entrée.
Choix pyrucast : un seul jeu d’identités par domaine géométrique, plusieurs
configurations pour les variantes (référence / déformée / prédite). Des
Coords distincts restent prévus pour les vrais domaines indépendants
(sous-domaines, maillages adaptés de plus haute densité).
Permutation solveur
Une permutation optionnelle (Vec<u32>, longueur = capacity) sépare
l’ordre solveur de l’identité : permutation[node_id] donne l’ordre
solveur associé. Elle est posée par l’appelant aujourd’hui ; une renumérotation
réduisant la bande/profil (Cuthill–McKee) la calculera. L’identité (NodeId)
n’est jamais modifiée, dans les deux cas.
Rust :
#[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 (the automatic computation is still to be written).
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());
}
Python :
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])
# Set a permutation by hand.
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
API Python
import pyrucast
c = pyrucast.Coords(dim=2)
n = c.add_node([0.0, 0.0]) # n is a pyrucast.Node; refcount = 1
m = c.add_node([1.0, 0.0])
print(c) # Coords: dim=2, configs=1 (active="default"), nodes=2 ...
n.set_position([0.5, 0.5])
# GC touches nothing as long as at least one Python Node exists.
assert c.gc() == 0
# del + collect force le Drop côté Rust et libère le refcount.
import gc as pygc
del n
pygc.collect()
assert c.gc() == 1
Méthodes d’inspection utiles : node_count() (nœuds vivants), capacity()
(slots alloués, vivants + non encore collectés), is_alive(id) et
refcount(id) — qui prennent un id brut (pas un Node) : un Node
portant un refcount ne pourrait jamais être observé mort. acquire(id) rend un
Node supplémentaire pour un id existant (refcount += 1). dump() imprime le
contenu intégral (coordonnées, configurations) sur la sortie standard.