Évolution (Evolution / SubEvolution)
Une évolution associe une série de valeurs à une variable (souvent
le temps, mais pas nécessairement) et interpole linéairement entre les
échantillons tabulés. C’est l’analogue de l’EVOLUTIO de Cast3M, généralisé :
l’abscisse n’est pas forcément le temps et la valeur peut être un champ
entier, pas seulement un réel.
Elle suit la même grammaire d’agrégat que tous les conteneurs de pyrucast (cf. Agrégat) :
SubEvolution— une courbe tabulée : une liste d’abscisses (triées, strictement croissantes) et la liste des valeurs en regard. Une valeur est un scalaire, unSubNodeField(champ aux nœuds) ou unSubElementField(champ aux points de Gauss) — toutes du même type, et pour les champs sur le même support. Son interpolation enxrend une valeur.Evolution— l’agrégat : une liste deSubEvolution, une par zone, exactement comme unNodeFieldagrège desSubNodeField. Son interpolation enxinterpole chaque courbe, puis regroupe les sous-champs résultants en unNodeField/ElementField. Pour des scalaires, elle rend une liste de flottants (il n’existe pas d’agrégat de flottant).
Evolution (agrégat)
├── SubEvolution zone 0 ── abscisses [t₀, t₁, …] × valeurs [v₀, v₁, …]
├── SubEvolution zone 1 ── …
└── …
Interpolation linéaire
Entre deux échantillons encadrants x_lo ≤ x ≤ x_hi, le résultat est le mélange
v_lo·(1−t) + v_hi·t avec t = (x − x_lo) / (x_hi − x_lo). Pour les champs, le
mélange réutilise l’arithmétique de champs (map_all + merge_components
précédé de check_same_components, cf.
Champ) : aucune logique numérique n’est dupliquée, et la
compatibilité des supports/composantes des deux champs encadrants est vérifiée à
ce moment-là. Une abscisse tombant exactement sur un échantillon rend la valeur
telle quelle.
Types d’abscisse et d’ordonnée
Une évolution peut porter le type physique de ses axes :
abscissa_type— le type de l’abscisse (p. ex."T","time"). Valable pour toutes les évolutions. Il sert à étiqueter les tracés (axe X d’une courbe, slider d’un champ) et, lorsqu’on interpole un champ, à choisir la composante du champ à lire (voir ci-dessous).ordinate_type— le type de la valeur, pour les évolutions scalaires uniquement (p. ex."young"). Il étiquette l’axe Y et nomme la composante produite quand on interpole un champ. Le donner sur une évolution de champs est une erreur (un champ a déjà ses propres composantes).
se = pc.SubEvolution(
[(0.0, 0.0), (100.0, 210e9)], abscissa_type="T", ordinate_type="young"
)
Interpoler un champ (courbe de transfert)
Une évolution scalaire à une seule courbe s’utilise comme une fonction de
transfert y = f(x) : au lieu d’un scalaire, on lui passe un champ et elle
rend un autre champ de même support, où chaque nœud / point de Gauss est
l’interpolation de la valeur d’entrée sur la courbe.
- La composante lue dans le champ d’entrée est celle nommée comme
l’
abscissa_type— la correspondance de type est vérifiée : si le champ n’a pas de composante de ce nom, c’est une erreur. - Le champ de sortie a une seule composante, nommée d’après l’
ordinate_type(à défaut"value"). - La politique hors-plage s’applique valeur par valeur, comme pour un scalaire.
# Loi matériau E(T) : module d'Young fonction de la température.
loi = pc.Evolution(
[(0.0, 0.0), (100.0, 210e9)], abscissa_type="T", ordinate_type="young"
)
young = loi.interpolate(temperature) # temperature : NodeField de composante "T"
# young : NodeField de composante "young"
Côté Evolution (agrégat), l’appel exige une seule courbe scalaire (sans
quoi le choix de la courbe serait ambigu) ; une SubEvolution s’interpole
directement.
Politique hors plage
Chaque évolution porte une politique appliquée quand l’abscisse demandée
sort de l’intervalle tabulé [x_min, x_max] :
| Politique | Effet hors plage |
|---|---|
"error" (défaut) | lève une erreur |
"clamp" | renvoie la valeur de l’extrémité la plus proche (pas d’extrapolation) |
"extrapolate" | prolonge linéairement avec le segment extrême |
La politique stockée peut être surchargée à l’appel :
evol.interpolate(x, out_of_range="clamp").
Construction
Deux voies, le constructeur haut niveau n’étant que du sucre au-dessus du
primitif bas niveau (motif model.heat_conduction(fes) / SubModel + |) :
- temps-major (haut niveau) —
Evolution([(t0, champ0), (t1, champ1), …])avec unNodeField/ElementField/ flottant complet par pas ; les champs entiers sont transposés en une courbe par zone (zones appariées entre pas par leur support, identique d’un pas à l’autre) ; - zone-major (bas niveau) — construire chaque
SubEvolutiondepuis sa liste(abscisse, sous-champ), puis agréger avec|.
L’union | et le slicing réinitialisent la politique hors-plage de
l’agrégat à "error".
API Rust
#[test]
fn une_evolution_interpole_scalaires_et_champs() -> Result<()> {
// Courbe scalaire X→Y : 0→10, 1→20.
let se = SubEvolution::new(
vec![(0.0, SubValue::Scalar(10.0)), (1.0, SubValue::Scalar(20.0))],
OutOfRange::Error,
)?;
match se.interpolate(0.5, None)? {
SubValue::Scalar(v) => assert_eq!(v, 15.0),
_ => unreachable!(),
}
// Hors plage : Error (défaut) lève ; surcharge Clamp → extrémité.
assert!(se.interpolate(2.0, None).is_err());
// Agrégat scalaire → liste de flottants.
let e = Evolution::from_scalars(vec![(0.0, 10.0), (1.0, 20.0)], OutOfRange::Error)?;
match e.interpolate(0.5, None)? {
Interpolated::Scalars(v) => assert_eq!(v, vec![15.0]),
_ => unreachable!(),
}
Ok(())
}
API Python
import pyrucast as pc
# Scalar curve (one SubEvolution).
se = pc.SubEvolution([(0.0, 10.0), (1.0, 20.0)])
print(se.interpolate(0.5)) # 15.0
print(se.interpolate(2.0, out_of_range="clamp")) # 20.0 (otherwise: an error)
# Agrégat scalaire → liste de flottants.
e = pc.Evolution([(0.0, 10.0), (1.0, 20.0)])
print(e.interpolate(0.5)) # [15.0]
# Low level: composing per-zone curves with `|`.
agg = pc.SubEvolution([(0.0, 1.0), (1.0, 2.0)]) | pc.SubEvolution(
[(0.0, 3.0), (1.0, 4.0)]
)
print(agg.interpolate(0.5)) # [1.5, 3.5]
# High level, time-major: one whole NodeField per step → interpolated NodeField.
ev = pc.Evolution([(0.0, champ_t0), (2.0, champ_t1)])
champ = ev.interpolate(1.0) # NodeField halfway
# Courbe de transfert : passer un champ → champ (loi matériau E(T)).
loi = pc.Evolution(
[(0.0, 0.0), (100.0, 210e9)], abscissa_type="T", ordinate_type="young"
)
young = loi.interpolate(temperature) # composante "T" lue → composante "young"
Tracé
evolution.plot(...) visualise l’évolution : courbe X-Y pour des scalaires, champ + slider de
valeur tabulée pour des champs. Voir Visualisation › Tracé d’une évolution.
À défaut de x_label / y_label explicites, les étiquettes reprennent
l’abscissa_type (axe X d’une courbe, slider d’un champ) et l’ordinate_type
(axe Y d’une courbe).
e = pc.Evolution([(0.0, 10.0), (1.0, 20.0), (2.0, 5.0)])
e.plot(save="courbe.svg", x_label="temps", y_label="T") # courbe scalaire
ev.plot(save="frame.png", frame=1) # tabulated field (one step)
Place dans le modèle
SubValue est un enum de stockage inline (scalaire / SubNodeField /
SubElementField), comme SubModel l’est pour les physiques. SubEvolution
s’adresse par un Handle<SubEvolution> et sérialise ses valeurs en ligne
via le trait Portable ; les courbes sont donc portables
comme tout autre objet. L’homogénéité du type de valeur est garantie
à la construction (au sein d’une courbe) et par check_push (entre zones d’un
même agrégat).