Agrégat
La plupart des conteneurs de pyrucast viennent par paires : un objet
zone (SubMesh, SubFiniteElementSpace, SubNodeField,
SubElementField, SubModel, SubMatrix, SubEvolution) et son agrégat
(Mesh, FiniteElementSpace, NodeField, ElementField, Model, Matrix,
Evolution). Tous les agrégats partagent exactement la même grammaire
d’accès et la même
composition par union — factorisées dans le trait Rust Aggregate
(src/aggregate.rs). Ce chapitre décrit ce contrat commun une fois pour
toutes ; chaque chapitre d’objet n’en redonne que les spécificités.
Principe : une liste de handles
Un agrégat est, au fond, un Vec<Handle<Sub>> : une liste de handles vers
des sous-objets adressés par handle. Il ne copie jamais ses zones — il en
partage les handles (refcount). Le trait Aggregate dérive toute la
mécanique d’accès de deux accesseurs seulement (items() / items_mut()), si
bien qu’ajouter un nouvel agrégat ne duplique aucun code d’indexation.
Mesh FiniteElementSpace Model
├── Handle<SubMesh> ├── Handle<SubFES> ├── Handle<SubModel>
├── Handle<SubMesh> ├── Handle<SubFES> └── Handle<SubModel>
└── … └── …
Interface uniforme
Tout agrégat expose, côté Python :
| Opération | Sens |
|---|---|
len(agg) | nombre de zones |
agg[i] | vue typée sur la zone i (un Sub…) — jamais une copie |
agg[i:j:k] | nouvel agrégat du même type avec le sous-ensemble des zones (slicing Python : pas, bornes négatives) |
for sub in agg: | itère les zones (via le protocole séquence) |
agg.unit() | la seule zone d’un agrégat unitaire, sinon une erreur claire |
agg.add_sub(sub) | ajoute une zone en place |
agg.add_subs(other) | concatène en place toutes les zones d’un autre agrégat (sans déduplication) |
agg | other | union (voir plus bas) |
repr / str / dump() | les trois niveaux d’affichage |
Côté Rust, le trait Aggregate fournit les mêmes : len, is_empty,
get(i), subset(indices), iter, unit, push/add_sub/add_subs, plus
Index<usize> et IntoIterator via la macro impl_aggregate_std_traits!.
agg[i]vue,agg[i:j]agrégat. L’indexation entière renvoie une vue sur une zone (unSub…) ; le slicing renvoie un agrégat du même type queagg, dont les zones sont des handles partagés (pas de copie). Le slicing s’appuie sursubsetcôté Rust et préserve les invariants de l’agrégat (check_push,finalize).
Passer une seule zone à un opérateur. Les opérateurs (
ops/) prennent des agrégats, pas des vues :mesh.invert(me[1])lève unTypeError(«SubMeshobject is not an instance ofMesh»). Pour n’en traiter qu’une, utiliser le slicing, qui rend un agrégat unitaire :mesh.invert(me[1:2]).
unit()vs[0].agg[0]prend silencieusement la première de plusieurs zones ;agg.unit()exige qu’il n’y en ait qu’une et lève sinon. Aux frontières qui n’ont de sens que mono-zone (par exemple ajouter une cellule à un maillage qu’on vient de créer avec un seul sous-maillage), préférerunit()— plus honnête (cf.CONVENTIONS.md).
Les sous-objets ne se construisent pas seuls
Un Sub… obtenu par agg[i] est une vue : on ne le construit pas
directement côté Python. On construit toujours au niveau parent
(Mesh(coords, type), FiniteElementSpace(mesh), ElementField(fes, comps))
ou par un opérateur qui rend un parent (model.heat_conduction(fes)…), puis on
indexe pour atteindre une zone.
Composition : l’union |
Composer deux agrégats, c’est l’union : a | b côté Python, a.union(&b)
côté Rust. La sémantique est uniforme pour les sept agrégats :
- Déduplication par handle. Une zone dont le
Handledésigne un objet déjà présent (Handle::same_object) n’est pas ajoutée deux fois. L’ordre est celui de première apparition. - Partage, pas copie. Les zones retenues sont partagées (refcount), jamais dupliquées en mémoire.
- Contraintes de domaine. Les invariants (même
Coordspour unMesh, etc.) restent vérifiés au moment de l’ajout (check_push). - Finalisation. Un crochet
finalize()tourne en fin d’union : no-op pour la plupart des agrégats, mais les champs le surchargent pour fusionner les zones partageant un même support (voir Champ, Champ aux nœuds).
Les quatre formes de l’union :
| Python | Rust | Résultat |
|---|---|---|
agg | agg | a.union(&b) | union dédupliquée des deux listes |
agg | sub | a.union_sub(&h) | ajoute une zone en queue (ignorée si déjà présente) |
sub | agg | a.union_sub_first(&h) | la même union, la zone en tête |
sub | sub | T::union_subs(&a, &b) | un agrégat neuf portant les deux zones |
L’union est donc acceptée dans les deux sens : sub | agg passe par le
__ror__ de l’agrégat (une zone seule ne sait unir qu’une autre zone), et ne
diffère de agg | sub que par l’ordre des zones. La déduplication et la
finalisation sont identiques — une zone déjà présente est simplement déplacée
en tête.
En place :
add_sub/add_subs. L’union rend un agrégat neuf et laisse ses deux opérandes intacts. Pour modifier l’agrégat courant, les deux primitives bas niveau sontagg.add_sub(sub)(une zone) etagg.add_subs(other)(toutes les zones d’un autre agrégat, dans l’ordre). Elles vérifientcheck_pushmais ne dédupliquent pas et n’appellent pasfinalize: c’est de la concaténation. L’API d’usage pour composer reste|(cf.CONVENTIONS.md).
Plus, pour les nœuds (cf. Nœud) :
| Python | Résultat |
|---|---|
node | node | Mesh POI1 unitaire sur les deux nœuds |
mesh | node | ajoute un point (erreur si Mesh non unitaire POI1) |
|compose,+calcule. L’union est toujours|. L’opérateur+(et-,*,/) est réservé à l’arithmétique des champs (field + 2.0, addition de champs…) — il n’est jamais utilisé pour composer des agrégats. Les deux ne se télescopent donc jamais. C’est un changement par rapport aux toutes premières versions, où la composition passait par+; aujourd’hui+est entièrement libéré pour le calcul.
Exemple
import pyrucast
c = pyrucast.Coords(dim=2)
ns = [c.add_node(p) for p in [(0, 0), (1, 0), (1, 1), (0, 1)]]
tri = pyrucast.Mesh(c, "TRI3")
tri.unit().add_cell([ns[0], ns[1], ns[2]])
qua = pyrucast.Mesh(c, "QUA4")
qua.unit().add_cell([ns[0], ns[1], ns[2], ns[3]])
# Union of two meshes (zones shared by handle).
mesh = tri | qua
print(len(mesh)) # 2 sous-maillages
print(mesh) # Mesh: 2 submesh(es), 2 cell(s) total
# In-place addition: `add_sub` for one zone, `add_subs` for all those of
# another aggregate (concatenation, without deduplication).
tri.add_subs(qua)
print(len(tri)) # 2 sous-maillages
Pourquoi un trait commun ?
Factoriser l’accès et l’union dans Aggregate garantit qu’un sous-maillage,
un sous-espace EF ou un sous-modèle s’indexent, s’itèrent et se composent
strictement de la même façon. Aucun __getitem__ n’est réécrit par type,
aucun comportement d’union ne diverge d’un agrégat à l’autre, et un nouvel
agrégat se câble en deux macros (impl_aggregate! + impl_aggregate_pymethods!).
Le seul point de personnalisation est finalize() (et check_push()), que les
champs exploitent pour leur fusion par support.