From 760f9eb71ff7185ffbed3d10a69d17c3706603bb Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Thu, 9 Jul 2026 16:21:14 +0200 Subject: [PATCH] Plotting: geometry-aware auto weights for Stacked/Paired --- CHANGELOGS.md | 12 ++++++++ Project.toml | 2 +- src/Plotting/combinators.jl | 40 +++++++++++++++---------- test/suite/plotting/test_combinators.jl | 23 +++++++++++--- 4 files changed, 57 insertions(+), 20 deletions(-) diff --git a/CHANGELOGS.md b/CHANGELOGS.md index 1cea3934..1792d756 100644 --- a/CHANGELOGS.md +++ b/CHANGELOGS.md @@ -5,6 +5,18 @@ All notable changes to CTBase will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.27.1-beta] - 2026-07-09 + +### 🔧 Changed + +- **`Plotting`: geometry-aware `:auto` weights.** `Stacked` / `Paired` now resolve + `weights=:auto` from the child's extent along the combinator axis — number of **rows** + for `Stacked`, number of **columns** for `Paired` (`grid_shape(child)[dim]`) — instead + of its leaf count. This is unchanged for flat columns (rows == leaf count) but keeps + cell heights uniform when a paired block is nested inside a stack, e.g. + `Stacked(Paired(state, costate), control, …)` now gets row weights `n : l : …` rather + than `2n : l : …`. Needed by the CTModels case layer (Phase 3); CTFlows is unaffected. + ## [0.27.0-beta] - 2026-07-09 ### ✨ New Features diff --git a/Project.toml b/Project.toml index 45d77a03..b17f1489 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "CTBase" uuid = "54762871-cc72-4466-b8e8-f6c8b58076cd" -version = "0.27.0-beta" +version = "0.27.1-beta" authors = ["Olivier Cots ", "Jean-Baptiste Caillau "] [deps] diff --git a/src/Plotting/combinators.jl b/src/Plotting/combinators.jl index bd73bb14..d366f895 100644 --- a/src/Plotting/combinators.jl +++ b/src/Plotting/combinators.jl @@ -4,17 +4,25 @@ # These are pure, typed tree builders over already-lowered layout NODES (see # lowering.jl for Panel -> node). They add no geometry beyond weights. # -# Weight policy `:auto` (decision D4): a child's weight is its number of drawable -# cells (`n_leaves`). Stacking N panels of k components each thus gives every cell -# the same height (proportional to the number of lines); explicit weights override. +# Weight policy `:auto` (decision D4) is geometry-aware: a child's weight is its +# extent along the combinator's axis — its number of rows for `Stacked` (vertical), +# its number of columns for `Paired` (horizontal). Stacking panels of different +# heights thus keeps every cell the same height; pairing columns keeps every column +# the same width. This is `grid_shape(child)[dim]`, which equals `n_leaves` for flat +# columns but stays correct when a paired block is nested inside a stack (e.g. the +# CTModels `Stacked(Paired(state, costate), control, …)` gets row weights `n : l : …` +# rather than leaf-count `2n : l : …`). Explicit weights always override. # # Docstrings deferred (Handbook convention). # ============================================================================= -# Resolve `:auto` weights to a concrete Float64 vector; otherwise pass through -# (the HBox/VBox constructor validates length and positivity). -function _resolve_weights(children::AbstractVector{<:AbstractLayoutNode}, weights) - weights === :auto && return Float64[n_leaves(c) for c in children] +# Resolve `:auto` weights to a concrete Float64 vector along axis `dim` (1 = rows, +# 2 = columns); otherwise pass through (the HBox/VBox constructor validates length +# and positivity). +function _resolve_weights( + children::AbstractVector{<:AbstractLayoutNode}, weights, dim::Integer +) + weights === :auto && return Float64[grid_shape(c)[dim] for c in children] return collect(Float64, weights) end @@ -22,23 +30,25 @@ end $(TYPEDSIGNATURES) Stack `children` vertically into a [`VBox`](@ref). With `weights=:auto` (default, -decision D4) each child's weight is its number of cells, so stacking panels of -different heights keeps every cell the same height; pass an explicit weight vector -to override. +decision D4) each child's weight is its number of **rows** (`grid_shape(child)[1]`), +so stacking blocks of different heights keeps every cell the same height — including +when a child is itself a paired block; pass an explicit weight vector to override. """ function Stacked(children::AbstractVector{<:AbstractLayoutNode}; weights=:auto) - return VBox(children, _resolve_weights(children, weights)) + return VBox(children, _resolve_weights(children, weights, 1)) end """ $(TYPEDSIGNATURES) -Place `children` side by side into an [`HBox`](@ref) (e.g. state | costate). Same -`:auto` weight policy as [`Stacked`](@ref). A two-argument form -`Paired(left, right)` is provided for the common pair. +Place `children` side by side into an [`HBox`](@ref) (e.g. state | costate). With +`weights=:auto` each child's weight is its number of **columns** +(`grid_shape(child)[2]`), so equal-width columns stay equal; pass an explicit weight +vector to override. A two-argument form `Paired(left, right)` is provided for the +common pair. """ function Paired(children::AbstractVector{<:AbstractLayoutNode}; weights=:auto) - return HBox(children, _resolve_weights(children, weights)) + return HBox(children, _resolve_weights(children, weights, 2)) end function Paired(left::AbstractLayoutNode, right::AbstractLayoutNode; weights=:auto) return Paired(AbstractLayoutNode[left, right]; weights=weights) diff --git a/test/suite/plotting/test_combinators.jl b/test/suite/plotting/test_combinators.jl index afaa545b..31b4e8b9 100644 --- a/test/suite/plotting/test_combinators.jl +++ b/test/suite/plotting/test_combinators.jl @@ -12,12 +12,20 @@ _col(k) = Plotting.VBox([_leaf() for _ in 1:k], ones(k)) # a column of k cells function test_combinators() Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "Plotting combinators" begin - Test.@testset "Stacked :auto weights ∝ cells" begin + Test.@testset "Stacked :auto weights ∝ rows" begin # a 2-cell column and a single leaf -> auto weights [2, 1] s = Plotting.Stacked(Plotting.AbstractLayoutNode[_col(2), _leaf()]) Test.@test s isa Plotting.VBox Test.@test s.weights == [2.0, 1.0] Test.@test Plotting.n_leaves(s) == 3 + + # discriminating case: a paired block (2 columns of 2 cells) stacked over a + # single leaf. Row-based auto gives [2, 1] (the block is 2 rows tall), NOT + # the leaf count [4, 1] — this is the CTModels state|costate / control case. + block = Plotting.Paired(_col(2), _col(2)) + s2 = Plotting.Stacked(Plotting.AbstractLayoutNode[block, _leaf()]) + Test.@test s2.weights == [2.0, 1.0] + Test.@test Plotting.n_leaves(s2) == 5 end Test.@testset "Stacked explicit weights" begin @@ -31,13 +39,20 @@ function test_combinators() end Test.@testset "Paired" begin + # two single columns -> one column each -> equal widths [1, 1], whatever + # their heights (width follows columns, not cell counts). p = Plotting.Paired(_col(2), _col(2)) Test.@test p isa Plotting.HBox - Test.@test p.weights == [2.0, 2.0] + Test.@test p.weights == [1.0, 1.0] Test.@test Plotting.n_leaves(p) == 4 - # unequal columns -> auto widths follow cell counts p2 = Plotting.Paired(_col(3), _col(1)) - Test.@test p2.weights == [3.0, 1.0] + Test.@test p2.weights == [1.0, 1.0] + + # discriminating case: a 2-column block beside a single column -> auto + # widths [2, 1] (columns), not the leaf count. + block = Plotting.Paired(_col(1), _col(1)) # 2 columns wide + p3 = Plotting.Paired(Plotting.AbstractLayoutNode[block, _col(1)]) + Test.@test p3.weights == [2.0, 1.0] end Test.@testset "Grid" begin