Plot engine
#363
Replies: 1 comment
-
|
Closed by #367. |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
Rapport : moteur de tracé unifié dans CTBase (CTModels + CTFlows)
Partie 1 — Objectif et vision
L'enjeu
Deux implémentations de tracé coexistent dans l'écosystème :
CTModels (
ext/CTModelsPlots.jl+plot.jl~1780 lignes +plot_default.jl+plot_utils.jl) : monolithe historique, riche fonctionnellement (5 groupes designaux, décorations, grilles de temps multiples, tracés de phase), mais où la
logique de mise en page, le rendu Plots et le sens métier sont entremêlés.
CTFlows (
ext/CTFlowsPlots/) : refonte récente déjà séparée en deux couches —PlotEngine(moteur générique, domain-free, ~250 lignes) etTrajectoryPlots(couche cas : vocabulaire trajectoire → panels). Le moteur y est explicitement
annoncé comme « destiné à être remonté dans CTBase ».
Objectif : un moteur unique, complet et conséquent hébergé par CTBase, et des
couches cas simples dans CTModels et CTFlows (et demain CTDirect, CTParser…). Le
moteur absorbe tout ce qui est mise en page, style, heuristiques et rendu ; les couches
cas ne font plus que : nommer, échantillonner, choisir un gabarit.
Les deux exigences structurantes
Layout général : la mise en page de CTModels est strictement plus riche que ce
que le
PlotEnginede CTFlows sait faire (blocs de hauteurs inégales, colonnesappariées + blocs empilés simultanément, grille 2×2 en
:group). Il faut doncplusieurs niveaux de définition d'un layout, du mot-clé simple à l'arbre
pondéré général (§ Partie 4).
Séparation backend : isoler dès maintenant ce qui est spécifique à Plots.jl de
ce qui ne l'est pas, pour qu'un backend Makie.jl soit un jour possible sans
toucher ni à l'IR ni aux couches cas (§ Partie 5). Concrètement : une
représentation intermédiaire (IR) de figure — pure donnée, zéro dépendance — dans
CTBase/src, et un renderer Plots dans une extensionCTBase/ext.Partie 2 — État des lieux
2.1 CTFlows :
PlotEngine+TrajectoryPlotsLe moteur (
PlotEngine/, ~250 lignes)Paneltitle,labels::Vector{String},data::Matrix{Float64}(n_times × n_comp),style::NamedTuplerender(times, panels; …)render!(p, times, panels; …)superpose sur une figure existante (ciblagesubplot=i):split(un subplot par composante) /:group(un subplot par panel, composantes superposées + légende):split):stacked(une colonne) /:paired(colonnes côte à côte, ex. state | costate) — repli sur:stackedsi hauteurs inégales:default/:normalizexlabeltemps sur la cellule du bas,ylabelpar composante, pas de légende en:split180px/ligne,340px/col, min 600), gardeylimspour séries constantes (tol. absolue1e-8), margesmm, fontesExceptions.IncorrectArgumentstructurées (layout, time, panels vides)La couche cas (
TrajectoryPlots/, ~260 lignes):state/:costate/:control+clean(pluriels, doublons) ;_default_descriptionsur les types abstraits de trajectoires ;la trajectoire en porte un (sinon
x₁, x₂…générés), contrôle:components/:norm/:all;Plots.plot/plot!sur les types concrets (pour gagner sur les stubsExtensionErrorabstraits).Bilan : l'architecture cible en miniature. Mais le moteur est trop petit pour
CTModels (voir § 3).
2.2 CTModels :
CTModelsPlots(monolithe)Inventaire fonctionnel complet — c'est le cahier des charges du futur moteur + couche cas :
Groupes de signaux et sélection
:state,:costate,:control,:path(contraintes de chemin),:dual(multiplicateurs) ;cleangère les alias (:states,:cons,:constraint(s)→:path,:duals→:dual) ;do_plot: contrôle jamais tracé sim == 0, path sinc == 0, dual si absent de la solution ;state_style, …,dual_style), valeur:none= groupe omis.Layout
:split: arbrePlotLeaf/PlotNode/EmptyPlot, nœuds:row/:columnoumatrices de layout avec hauteurs fractionnaires :
(state | costate)surnlignes, contrôle surllignes (l ∈ {m, 1, m+1}selon:components/:norm/:all),(path | dual)surnclignes — les poids relatifsn/(n+l),nc/nblinessont recalculés à l'assemblage (__initial_plot,plot.jl:265-470) ;:group: 1 ligne de subplots, sauf cas spécialcontrol=:allavec 4 plots →grille (2,2) ;
__size_plot(600×280 / 600×420 / 600×140·nblines).Décorations (absentes du moteur CTFlows)
t0/tfsur tous les subplots (time_style), positionsrecalculées si temps normalisé, temps initial/final éventuellement dépendant de la
variable de décision ;
(
*_bounds_style), ciblage du subplot par indice de composante,z_order=:back;do_decorate).Données et séries
time_grid(sol, :state|:control|:costate|:path)— modèles
UnifiedTimeGridModel/MultipleTimeGridModel(fusionunique(sort(vcat(T_x, T_y)))pour les tracés (x,y)) ; cas réellement exercé dans.extras/plot_manual.jl(T_costate = T[1:end-1],T_path = nothing, données enmatrices → interpolation constante) — ce n'est pas une possibilité théorique ;
seriestypepar défaut dépendant des données ::steppostsi l'interpolation ducontrôle est
:constant, sinon:path;ylimsà tolérance relative (1e-3), avec cas particulier milieu ≈ 0 ;Tracés de phase / paires (x, y)
@recipepublicf(sol, model, xx, yy, time)avecxx, yy ∈ {:time, :state, :control, :costate, :control_norm, :path_constraint, :dual_path_constraint}(+ indice de composante) — permetplot(sol, model, (:state,1), (:state,2))etc. ;__get_data_plot,_get_variable_values,_handle_*_plot) n'utilise pas Plots : c'est du calcul pur.Plomberie Plots
Plots.plot(sol, …),Plots.plot!(sol, …)(surcurrent()),Plots.plot!(p, sol, …);pest vide, la figure initiale estreconstruite puis substituée champ à champ par
setfield!(plot.jl:1200-1203) —point dur à préserver ou assainir ;
Plots.attributes(:Series)(
__keep_series_attributes) — purement Plots-specific.2.3 Divergences internes (à unifier au passage)
1e-81e-3+ cas milieu ≈ 0render:pathtoujours:steppostsi interpolation constantep₁, p₂…génériques"p" * x_labelChacun de ces points devra recevoir une politique dans le moteur (paramétrable), et
les différences résiduelles être assumées comme changement de comportement mineur.
Partie 3 — Pourquoi le
PlotEngineactuel ne suffit pasÉcarts bloquants du moteur CTFlows face au cahier des charges CTModels :
:pairedexige des colonnes de même hauteur et lemoteur ne sait pas empiler des blocs de hauteurs différentes avec des poids.
CTModels a besoin de
VStack( HStack(state, costate), control, HStack(path, dual) )avec des poids
n : l : nc. C'est la généralisation centrale.render(times, panels)impose une grille partagée ; lespanels path/dual de CTModels peuvent vivre sur une autre grille. Le temps doit
descendre dans le panel (voire dans la série).
hline/vlineattachée à une cellule ou à lafigure.
Panel.styleest global au panel ; leseriestypedu contrôleet les styles de bornes exigent une granularité par série.
tracés de phase demandent des séries x/y arbitraires (ce qui, une fois fait, couvre
aussi le cas temps).
:groupen grille. Pas de grille (2,2).Aucun de ces points n'est spécifique au contrôle optimal : ils appartiennent tous
légitimement au moteur.
Partie 4 — Le modèle de layout : trois niveaux
Réponse à l'exigence « plusieurs manières de définir un layout ». Trois niveaux
d'expression, chacun abaissé (lowering) vers le suivant ; le renderer ne voit que
le niveau 3. Un seul chemin de rendu, plusieurs portes d'entrée.
Niveau 1 — mots-clés (API actuelle, sucre)
Ce que les utilisateurs connaissent. La couche cas traduit
(:split, panels)en ungabarit de niveau 2. Compatibilité ascendante garantie : les signatures publiques
plot(sol; layout=:split, control=:norm, …)ne changent pas.Niveau 2 — combinateurs de blocs (déclaratif structuré)
Le gabarit CTModels
:splitcomplet s'écrit :C'est le niveau que manipulent les couches cas — lisible, testable, sans géométrie.
Niveau 3 — arbre pondéré général (IR, ce que rend le backend)
Équivalent backend-agnostique du
PlotNode/matrice de layout de CTModels — mais typé(pas de
Matrix{Any}), avec poids explicites et parcours déterministe (ordre denumérotation des cellules documenté, nécessaire pour
render!en superposition).Côté backends : Plots reproduit les poids par matrices de layout imbriquées (technique
déjà éprouvée dans
__plot_tree) ; Makie les mappe directement surGridLayout+rowsize!— l'arbre pondéré est même plus naturel en Makie qu'en Plots.Partie 5 — Architecture cible
5.1 Vue d'ensemble
5.2 Le point technique clé : pourquoi l'IR doit être dans
srcUne extension Julia n'est pas adressable proprement depuis un autre paquet
(
Base.get_extensionest fragile et non idiomatique). La solution canonique :(
render,render!) — avec stubsExtensionErrorquand aucun backend n'est chargé ;Plots (pattern « function ownership » standard des weak deps).
Les couches cas construisent l'IR avec les types de
CTBase.Plotting(toujoursdisponibles) et appellent
Plotting.render(backend, fig). CommeCTModelsPlotsestelle-même déclenchée par
Plots, dès qu'elle est chargée,CTBasePlotsl'est aussi(CTBase + Plots présents) — aucun
get_extensionnulle part.C'est exactement la séparation demandée : tout ce qui peut vivre sans Plots vit dans
src; seul le rendu vit dansext.5.3 L'IR (esquisse)
Plus haut dans le moteur (toujours
src) :Panel(groupe titré de composantes, commeaujourd'hui), les combinateurs de niveau 2, le lowering
Panel/gabarit →Axes/arbre,les heuristiques de taille, la normalisation du temps, la garde
ylims, les défauts__layout(),__time(),__style()(convention double-underscore du Handbook).Le
Panels'enrichit par rapport à CTFlows : il porte sa grille x (écart n°2) etautorise un style par composante (écart n°4). Les tracés de phase ne passent pas par
Panel: la couche cas construit directement unAxesavec desSeriesx/y — lemoteur les couvre par construction (écart n°5).
5.4 Le contrat backend — frontière Plots / non-Plots
HBox/VBoxGridLayout+rowsize!Series,HLine,VLineplot!(subplot=i),hline!,vline!lines!,hlines!,vlines!color,linewidth,linestyle,alpha,seriestype ∈ {:path, :steppost, :scatter},z_order ∈ {:front, :back}Plots.attributes(:Series):steppost→stairs(step=:post))ylims!,size=)Plots.font,Measures.mmfontsize,paddingPlots.current(), mutationsetfield!@recipe(interop recettes)Canal d'échappement : un kwarg
backend_kwargs::NamedTupletransmis tel quel aubackend (perte de portabilité assumée et documentée) — évite de faire grossir le
vocabulaire neutre au-delà du nécessaire (YAGNI).
5.5 Les couches cas, réduites à leur spécification
Chaque couche cas fournit exactement cinq choses :
clean(alias, doublons) + description par défaut (contrat surtypes abstraits, à la
_default_description) ;depuis le modèle (
state_components,time_name…), choisir leseriestype(interpolation du contrôle) — conventions métier incluses (ylabel costate vide si
state affiché,
‖u‖, préfixe « p ») ;HLine,t0/tf(y compris dépendants de la variable) →VLine;Plots.plot/plot!sur les types concrets, gating(
do_plot-like), gestion:none.Estimation : ~300–400 lignes par couche cas (contre ~2000 aujourd'hui côté
CTModels), et zéro géométrie.
Note : l'extraction de données des tracés de phase (
__get_data_plot,_get_variable_values, fusion des grilles multiples) n'a aucune dépendance à Plots ;elle gagnerait à migrer dans
CTModels/src(moduleDisplayou dédié), l'extensionn'en gardant que l'habillage. À trancher en phase 3 (D6).
Partie 6 — Conformité à la philosophie
Plotting(IR + contrat) /CTBasePlots(rendu) / couches cas — trois responsabilités, trois modulesusing Plots: Plots+Plots.plot!partout ;import CTBase.Exceptions; pas deusingnu (corrige au passageext/CTModelsPlots.jl)AbstractBackend/PlotsBackend(nom) ; layout/placement restent des options, pas des types (pas d'axe orthogonal justifiant un trait — KISS)rendersurAbstractBackend, stubsNotImplemented/ExtensionError; couches cas écrites contre types abstraitsIncorrectArgument(layout/time/control invalides),PreconditionError(poids ≠ enfants, panels vides),NotImplemented(contrat backend),ExtensionError(Plots absent)Matrix{Any}et tuples hétérogènes de__plot_tree) ; barrières de fonction au loweringTestabilité (tenet 9) — l'argument massue de cette architecture : l'IR étant de la
donnée pure, on teste sans backend ni display — unités sur
clean/lowering/poids/garde ylims/heuristiques, golden tests sur «
Figureattendue pour telle solution »,et une mince couche de tests de contrat par backend. Aujourd'hui, tester exige de
construire de vraies figures Plots.
Partie 7 — Risques et points durs
plot!(p, sol)sur figure vide (substitutionsetfield!)render!cible des cellules existantes par indexation déterministe de l'arbre ; le cas « figure vide » est traité en amont (créer au lieu de muter). Cas d'usage catalogués dans.extras/plot_manual.jl:153-159(plot!surcurrent(), surpltpré-dimensionné) et:228-239(comparaison de deux solutions, temps normalisé, styles par groupe). Documenter le changement s'il y en a unplot(sol; linestyle=:dash))__keep_series_attributes) +backend_kwargsexplicite pour le reste ; le mécanismePlots.attributes(:Series)est validé isolément dans.extras/plot_series.jlCTModelsPlotsrequiert Plots ⇒CTBasePlots(CTBase+Plots) se charge aussi ; n'appelerrenderqu'au runtime@recipepublic de CTModels (paires x/y)digits=2actuels)Plotting(revue de code sur ce critère)Partie 8 — Plan de migration (phases)
Chaque phase = un plan détaillé (PLAN.md)
validé avant code, tests verts et non-régression avant de passer à la suivante.
Docstrings en dernier de chaque phase.
Workflow git (s'applique à chaque unité de travail)
Une unité de travail = une phase, ou une sous-étape livrable indépendamment (ex.
3(a), 3(b)…). Pour chacune :
main:git checkout main && git pull && git checkout -b <feat/…>— jamais de travail directement sur
main.commit/push— aucune écriture distantesans validation explicite (règle CLAUDE.md du dépôt).
mainune fois l'accord donné.(
Project.toml+ CHANGELOG/BREAKING) pour propager vers l'amont — l'ordre depropagation suit le DAG :
CTBase→CTFlows/CTModels→CTDirect/OptimalControl.Chaque cran d'amont bumpe sa borne
[compat]sur le paquet aval mis à jour.Ordre de propagation typique pour ce chantier :
Phase 0 — Spécification et gel du comportement
(signatures, défauts, exemples de docs), captures de référence manuelles ;
Actifs existants : les scripts
.extras/(CTModels)Quatre scripts de vérification visuelle écrits à la main constituent déjà la matrice
de cas de référence. Décision actée : ils seront convertis en tests versionnés
(format module + fonction du Handbook,
test/suite/) pour que la matrice de gel tourneen CI et non plus seulement à l'œil ; les scripts restent comme parcours de validation
visuelle manuelle. Cette conversion est le premier livrable de la phase 0.
.extras/plot_duals.jl@def(CTParser) avec contraintes mixtes/boîtes ; toutes les combinaisons de description (:state…:dual, y compris:path, :dualseuls) ; matrice des*_style=:none(5 groupes) ; matrice des décorations désactivées (time_style,*_bounds_style) ;label/colorutilisateur.extras/plot_manual.jlPreModel/build_solutionavec grilles distinctes par composante et interpolation constante (matrices) ; matrice:group/:split×control=:components/:norm/:all; styles par groupe (NamedTuple) ; sémantiqueplot!(surcurrent(), surpltpré-dimensionné) ; superposition de deux solutions (temps normalisé, styles en tirets).extras/plot_variable.jltflibre,tf → min) avec contrainte de chemin surv; exerce le chemint0/tfdépendant de la variable (décorations temps non triviales) et une solution issue du solveur (pas fabriquée).extras/plot_series.jlPlots.attributes(:Series)(base de__keep_series_attributes)Ces scripts définissent en creux le contrat de compatibilité de la phase 3 : toute
ligne qui y figure doit produire une figure équivalente après migration. Ils serviront
aussi de banc d'essai pour choisir les politiques unifiées (D5) — les deux jeux de
gabarits (
:splitcomplet 5 groupes,:groupavec 2×2) y sont exercés. Note :plot_variable.jltire CTDirect/Ipopt ; sa version testée devra soit être isolée(dépendances de test lourdes), soit reconstruire une solution équivalente via
build_solution(commeplot_manual.jl) pour rester dans les dépendances de test deCTModels.
Phase 1 — CTBase :
Plotting+CTBasePlotsCTBase/src/Plotting/: IR, combinateurs N2, lowering, heuristiques,défauts
__…, contratrender/render!+ stubs ;CTBase/ext/CTBasePlots.jl: renderer Plots = port généralisé duPlotEnginede CTFlows (poids, x par panel, décorations, style par série,Grid) ;Phase 2 — CTFlows bascule (le cas simple d'abord)
TrajectoryPlotsconstruit l'IR CTBase ; suppression dePlotEnginelocal ;Phase 3 — CTModels bascule (le gros morceau)
Sous-étapes livrables indépendamment :
:split/:groupstate/costate/control (avec:components/:norm/:all) ;t0/tf) ;données vers
src(D6) ;plot!superposition/comparaison de solutions ;plot.jl/plot_default.jl/plot_utils.jlactuels.Phase 4 — Consolidation
deux paquets ;
Phase 5 (optionnelle, plus tard) — POC Makie
Un
CTBaseMakieminimal (quelques cellules, un empilement pondéré) uniquement pourvalider le contrat — pas un backend complet (YAGNI).
Partie 9 — Décisions à trancher
Plotting(CTBase.Plotting) — pas de collision avec Plots.jl ; alternatives :Figures,PlotSpeccolor,linewidth,linestyle,alpha,seriestype,z_order) +backend_kwargsen échappatoire‖u‖)Stacked:auto= proportionnel au nombre de lignes (comportement CTModels actuel) ; poids explicites possiblesCTModels/srcAnnexe — Cartographie code actuel → code cible
CTFlows/ext/…/PlotEngine/panel.jl(Panel,__layout,__time,__style)CTBase/src/Plotting/(enrichi : x par panel, style par composante)CTFlows/ext/…/PlotEngine/render.jl(render,render!, cells, tailles, ylims)CTBase/src/Plotting/, dessin →CTBase/ext/CTBasePlots.jlCTModels/ext/plot.jl:1-46(PlotLeaf/PlotNode/EmptyPlot)Leaf/HBox/VBoxtypés dans l'IRCTModels/ext/plot.jl:215-251(__plot_tree)CTModels/ext/plot.jl:265-470(__initial_plot)CTModels/ext/plot.jl:505-1060(__plot!: séries + décorations)CTModels/ext/plot.jl:68-159(__plot_time!: ylims, steppost, labels)CTModels/ext/plot.jl:1421-1779(recipe + extraction (x,y))CTModels/src(D6)CTModels/ext/plot_utils.jl(clean,do_plot,do_decorate)CTModels/ext/plot_default.jl(__size_plot, défauts)CTFlows/ext/…/TrajectoryPlots/Beta Was this translation helpful? Give feedback.
All reactions