Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -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 <olivier.cots@irit.fr>", "Jean-Baptiste Caillau <caillau@univ-cotedazur.fr>"]

[deps]
Expand Down
40 changes: 25 additions & 15 deletions src/Plotting/combinators.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,51 @@
# 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

"""
$(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)
Expand Down
23 changes: 19 additions & 4 deletions test/suite/plotting/test_combinators.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading