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 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, le Coords reste en mémoire ;
  • tant qu’au moins un Node (ou un objet aval comme Mesh / Field via incref/decref) référence un NodeId, 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èleAvantagesLimites
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.