Sauvegarde et relecture
Écrire des objets dans un fichier, et les relire en gardant ce qu’ils partageaient. Un dictionnaire à l’aller, un dictionnaire au retour.
pyrucast.save(
"etude.pyr",
{
"maillage fin": mesh,
"T (°C)": temperature,
"materiaux": mat,
"time step": 0.05,
"instants": [0.0, 0.1, 0.2],
},
)
objets = pyrucast.load("etude.pyr")
mesh2 = objets["maillage fin"]
t2 = objets["T (°C)"]
Les clefs sont libres : un espace, un accent, une unité — tout ce qu’une chaîne Python peut porter. C’est la raison du dictionnaire explicite plutôt que des mots-clefs.
La garantie : le partage survit
C’est la propriété pour laquelle ce format existe. Deux champs bâtis sur un même support sont relus sur un support, pas sur deux copies aux mêmes nœuds :
t = pyrucast.NodeField(support, ["T"])
f = pyrucast.NodeField(support, ["f"])
pyrucast.save("etude.pyr", {"T": t, "f": f})
o = pyrucast.load("etude.pyr")
assert len(o["T"] | o["f"]) == 1 # a single zone: the support is one object
L’union fusionne les zones qui partagent un support et laisse côte à côte celles qui n’en partagent pas — c’est l’observable la plus directe du partage. Sans cette garantie, un champ et son maillage relus ne combineraient plus : ils porteraient les mêmes nœuds sans être le même support, et toute l’arithmétique de champs tomberait à côté.
Le mécanisme tient en une phrase : les objets sont écrits sous des identifiants locaux au fichier, et un objet référencé deux fois n’est écrit qu’une fois. Une adresse mémoire n’aurait aucun sens dans un autre processus ; un numéro de fichier, si.
On donne les racines, pas la liste des dépendances
save prend ce qui vous intéresse. Ce dont ces objets ont besoin suit tout seul :
pyrucast.save("m.pyr", {"maillage": mesh}) # also writes the Coords and the submeshes
Il n’y a rien à énumérer, et rien à oublier.
Relire ajoute, ne remplace pas
load fabrique des objets neufs et vous rend le dictionnaire. Ce qui vivait déjà dans votre session n’est pas touché : on peut relire deux fois le même fichier et obtenir deux graphes indépendants, ou relire un maillage à côté de celui qu’on manipule.
Ce que le fichier ne porte pas
La règle est simple : ce qui se recalcule ne s’écrit pas.
Sortent donc du fichier tous les caches et toutes les mémoïsations — la matrice assemblée, la factorisation du solveur, le coloriage des mailles, les tables d’index paresseuses, la copie qu’un champ garde de la connectivité de son support. Tout cela se rebâtit à la première demande, exactement comme sur un graphe construit à la main :
o = pyrucast.load("etude.pyr")
k = pyrucast.matrix.stiffness(o["modele"], o["materiaux"]) # réassemble
u = pyrucast.solver.solve(k, o["chargement"]) # refactorise
Ce n’est pas une économie de place accessoire : la copie qu’un champ nodal garde de sa connectivité pèse autant que le maillage lui-même, et un fichier qui la porterait la porterait une fois par champ.
Les compteurs de références
Ils ne sont pas écrits non plus, et c’est délibéré : un fichier contient certains des objets qui référencent un nœud, pas tous. Un compteur sauvé décrirait un monde qui n’existe plus.
À la relecture, tout est recompté depuis zéro. Les objets se comptent seuls — chaque référence rendue par load compte pour une. Les nœuds sont réincrémentés par les sous-maillages relus.
Conséquence à connaître : un nœud relu n’est protégé que par les objets présents dans le fichier. Un Node que vous teniez dans une variable Python n’est pas archivé — c’est un atome de votre script, pas un objet du graphe. Sauver une Coords seule et la relire donne donc des nœuds à compteur nul, qu’un gc() collectera :
c2 = pyrucast.load("coords_seules.pyr")["c"]
c2.gc() # collects everything: nothing in the file held those nodes
C’est exactement l’état où l’on serait après avoir reconstruit les mêmes objets à la main sans en garder de Node. Pour qu’un nœud survive, sauvez ce qui l’utilise.
Les valeurs simples
À côté des objets, le fichier accepte un bool, un int, un float, une str, et les listes homogènes de ces quatre types. De quoi ranger le pas de temps, le nom du cas de charge ou la liste des instants avec les champs auxquels ils se rapportent.
Trois refus, nommés plutôt que silencieux : une liste imbriquée ou un dictionnaire (hors périmètre), une liste hétérogène, et un entier hors des 64 bits du format — les entiers Python sont non bornés, le fichier ne l’est pas.
Le fichier
b"PYRUCAST" signature, 8 octets
version de format (u32) toute autre valeur est refusée, jamais convertie
version du crate informative : elle sert au diagnostic, jamais au test
enregistrements (identifiant, type, octets), en ordre de dépendance
racines les clefs que vous avez données
Sauver deux fois les mêmes objets produit le même fichier, octet pour octet : les clefs sont triées, donc les identifiants sont distribués dans un ordre déterministe. Un fichier d’archive se compare, se met sous gestion de version, se hache.
Le format binaire est identique sous Linux et Windows : entiers petit-boutistes normalisés, usize sur 64 bits, f64 IEEE-754, aucun chemin ni séparateur dépendant du système dans les données.
Avant la version 1.0.0, le format peut changer sans préavis. Une version inconnue est refusée avec un message qui nomme les deux numéros — jamais décodée à moitié.
Ce n’est pas un format d’échange
Un fichier .pyr est le format de session de pyrucast : il sert à reprendre un calcul, pas à le publier. Pour donner des résultats à un autre outil, export_vtk existe pour ça, et ParaView le lit nativement.
La distinction n’est pas de la pudeur. Un format d’échange comme HDF5 apporte l’interopérabilité, la lecture partielle et la compression — mais aucune notion d’identité d’objet ni de référence partagée. La garantie du haut de cette page devrait y être reconstruite à l’identique, par-dessus. À l’inverse, un format de session n’a pas à être lisible par des tiers, et gagne à pouvoir casser tant que la bibliothèque n’est pas figée. Confondre les deux les abîme tous les deux.
Côté Rust
#[test]
fn sauver_et_relire_un_graphe_d_objets() -> Result<()> {
// `tempfile` is not a dependency of the project: a unique name in the system's
// temporary directory is enough.
let chemin = std::env::temp_dir().join(format!("pyrucast_doc_{}.pyr", std::process::id()));
let chemin = chemin.to_str().unwrap().to_string();
let chemin = chemin.as_str();
let coords = Handle::new(Coords::new(2)?);
let a = Node::create_in(coords.clone(), &[0.0, 0.0])?;
let b = Node::create_in(coords.clone(), &[1.0, 0.0])?;
let mut mesh = Mesh::from_submesh(SubMesh::new(coords, ElementType::SEG2));
mesh.add_cell(&[a.id(), b.id()])?;
let temperature = NodeField::new(&mesh::to_poi1(&mesh)?, vec!["T".into()])?;
archive::save(
chemin,
&[
("maillage fin", &mesh as &dyn archive::ArchiveRoot),
("T (°C)", &temperature),
("pas de temps", &0.05_f64),
],
)?;
let mut objets = archive::load(chemin)?;
let mesh2 = objets.mesh("maillage fin")?; // erreur nommant clef, type attendu, type trouvé
let dt = objets.float("pas de temps")?;
assert_eq!(mesh2.cell_count(), 1);
assert_eq!(dt, 0.05);
Ok(())
}
À l’écriture les types sont connus du compilateur, une tranche de paires suffit. À la relecture ils ne le sont pas : load rend une table nommée, dont on tire chaque objet avec son type attendu.
Le détail du mécanisme — la découverte des dépendances par la sérialisation elle-même, la détection de cycle, le crochet d’après-relecture — est dans Modèle mémoire et dans la documentation du module archive.