From ac0d5ede1719e1bf445c2e76bdbe89024eb29efb Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Sat, 11 Jul 2026 15:23:07 +0200 Subject: [PATCH 1/3] Wrap long code lines in guide documentation Reformat code blocks across all guide pages to stay within ~75 chars. Long function calls are wrapped with one argument per line and trailing commas. Also aerates the fake-strategies block in orchestration-and-routing.md and replaces ### headings with bold text in data.md to reduce TOC depth. --- docs/src/guide/api-documentation.md | 10 +- docs/src/guide/color-system.md | 9 +- docs/src/guide/coverage.md | 4 +- docs/src/guide/data.md | 194 +++++++++++++------- docs/src/guide/descriptions.md | 32 +++- docs/src/guide/differentiation.md | 57 ++++-- docs/src/guide/exceptions.md | 27 ++- docs/src/guide/implementing-a-strategy.md | 71 +++++-- docs/src/guide/options-system.md | 76 +++++--- docs/src/guide/orchestration-and-routing.md | 107 ++++++++--- docs/src/guide/strategy-parameters.md | 59 ++++-- docs/src/guide/test-runner.md | 24 ++- 12 files changed, 480 insertions(+), 190 deletions(-) diff --git a/docs/src/guide/api-documentation.md b/docs/src/guide/api-documentation.md index 9a3cd09d..43ca6b1b 100644 --- a/docs/src/guide/api-documentation.md +++ b/docs/src/guide/api-documentation.md @@ -160,7 +160,12 @@ CTBase.automatic_reference_documentation(; public=true, private=false, public_title="User API", - public_description="This page documents the public interface for end users. All functions listed here are stable and safe to use in your applications.", + public_description=""" + This page documents the public interface + for end users. All functions listed here + are stable and safe to use in your + applications. + """, filename="api", ) ``` @@ -436,7 +441,8 @@ jobs: - uses: actions/checkout@v2 - uses: julia-actions/setup-julia@v1 - name: Install dependencies - run: julia --project=docs -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()' + run: | + julia --project=docs -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()' - name: Build documentation run: julia --project=docs docs/make.jl - name: Deploy to GitHub Pages diff --git a/docs/src/guide/color-system.md b/docs/src/guide/color-system.md index 35e67e90..e8e24a52 100644 --- a/docs/src/guide/color-system.md +++ b/docs/src/guide/color-system.md @@ -227,8 +227,11 @@ This ensures your display code: ## See Also - [`Core.Style`](@ref), [`Core.Palette`](@ref) — type definitions -- [`Core.DEFAULT_PALETTE`](@ref), [`Core.MONOCHROME_PALETTE`](@ref), [`Core.HIGH_CONTRAST_PALETTE`](@ref) — built-in themes -- [`Core.current_palette`](@ref), [`Core.set_palette!`](@ref), [`Core.reset_palette!`](@ref) — palette switching +- [`Core.DEFAULT_PALETTE`](@ref), [`Core.MONOCHROME_PALETTE`](@ref), + [`Core.HIGH_CONTRAST_PALETTE`](@ref) — built-in themes +- [`Core.current_palette`](@ref), [`Core.set_palette!`](@ref), + [`Core.reset_palette!`](@ref) — palette switching - [`Core.set_color!`](@ref) — single-role override -- [`Core.get_format_codes`](@ref) — derive styled codes for custom `show` methods +- [`Core.get_format_codes`](@ref) — derive styled codes for + custom `show` methods - [`Core.show_palette`](@ref) — interactive preview of the active palette diff --git a/docs/src/guide/coverage.md b/docs/src/guide/coverage.md index cdfb4770..bc1b7a0e 100644 --- a/docs/src/guide/coverage.md +++ b/docs/src/guide/coverage.md @@ -27,8 +27,8 @@ To generate actionable coverage reports, we use a dedicated `coverage.jl` script ### Example `test/coverage.jl` ```julia -# Add the test directory to the load path so Julia can find dependencies from -# test/Project.toml. +# Add the test directory to the load path so Julia +# can find dependencies from test/Project.toml. pushfirst!(LOAD_PATH, @__DIR__) using Pkg diff --git a/docs/src/guide/data.md b/docs/src/guide/data.md index 0a2ea603..bb5c5ded 100644 --- a/docs/src/guide/data.md +++ b/docs/src/guide/data.md @@ -18,21 +18,37 @@ using LinearAlgebra ## Overview -| Type | Mathematical object | Natural signature (autonomous/fixed) | -|---|---|---| -| [`Data.VectorField`](@ref CTBase.Data.VectorField) | ``X : \mathcal{X} \to \mathbb{R}^n`` | `X(x)` | -| [`Data.Hamiltonian`](@ref CTBase.Data.Hamiltonian) | ``H : T^*\mathcal{X} \to \mathbb{R}`` | `H(x, p)` | -| [`Data.PseudoHamiltonian`](@ref CTBase.Data.PseudoHamiltonian) | ``\tilde{H} : T^*\mathcal{X} \times \mathcal{U} \to \mathbb{R}`` | `H̃(x, p, u)` | -| [`Data.ComposedHamiltonian`](@ref CTBase.Data.ComposedHamiltonian) | ``H : T^*\mathcal{X} \to \mathbb{R}`` | `H(x, p)` | -| [`Data.HamiltonianVectorField`](@ref CTBase.Data.HamiltonianVectorField) | ``\vec{H} : T^*\mathcal{X} \to \mathbb{R}^{2n}`` | `HVF(x, p)` | -| [`Data.ControlledVectorField`](@ref CTBase.Data.ControlledVectorField) | ``f_c : \mathcal{X} \times \mathcal{U} \to \mathbb{R}^n`` | `fc(x, u)` | -| [`Data.ComposedVectorField`](@ref CTBase.Data.ComposedVectorField) | ``g : \mathcal{X} \to \mathbb{R}^n`` | `g(x)` | -| [`Data.ControlLaw`](@ref CTBase.Data.ControlLaw) | ``u : \cdots \to \mathcal{U}`` | `u()` / `u(x)` / `u(x, p)` | -| [`Data.PathConstraint`](@ref CTBase.Data.PathConstraint) | ``g : \cdots \to \mathbb{R}^{n_g}`` | `g(x)` / `g(u)` / `g(x, u)` | -| [`Data.Multiplier`](@ref CTBase.Data.Multiplier) | ``\mu : T^*\mathcal{X} \to \mathbb{R}^{n_g}`` | `\mu(x, p)` | - -All ten share the same trait axes — time dependence and variable dependence. -`VectorField` and `HamiltonianVectorField` also carry a mutability trait; +| Type | Mathematical object | Out-of-place call | In-place | +|---|---|---|---| +| [`Data.VectorField`](@ref CTBase.Data.VectorField) | ``X : \mathcal{X} \to \mathbb{R}^n`` | `X([t, ]x[, v])` | ✓ | +| [`Data.Hamiltonian`](@ref CTBase.Data.Hamiltonian) | ``H : T^*\mathcal{X} \to \mathbb{R}`` | `H([t, ]x, p[, v])` | — | +| [`Data.PseudoHamiltonian`](@ref CTBase.Data.PseudoHamiltonian) | ``\tilde{H} : T^*\mathcal{X} \times \mathcal{U} \to \mathbb{R}`` | `H̃([t, ]x, p, u[, v])` | — | +| [`Data.ComposedHamiltonian`](@ref CTBase.Data.ComposedHamiltonian) | ``H : T^*\mathcal{X} \to \mathbb{R}`` | `H([t, ]x, p[, v])` | — | +| [`Data.HamiltonianVectorField`](@ref CTBase.Data.HamiltonianVectorField) | ``\vec{H} : T^*\mathcal{X} \to \mathbb{R}^{2n}`` | `HVF([t, ]x, p[, v])` | ✓ | +| [`Data.ControlledVectorField`](@ref CTBase.Data.ControlledVectorField) | ``f_c : \mathcal{X} \times \mathcal{U} \to \mathbb{R}^n`` | `fc([t, ]x, u[, v])` | — | +| [`Data.ComposedVectorField`](@ref CTBase.Data.ComposedVectorField) | ``g : \mathcal{X} \to \mathbb{R}^n`` | `g([t, ]x[, v])` | — | +| [`Data.ControlLaw`](@ref CTBase.Data.ControlLaw) | ``u : \cdots \to \mathcal{U}`` | `u([t, ]⋯[, v])`† | — | +| [`Data.PathConstraint`](@ref CTBase.Data.PathConstraint) | ``g : \cdots \to \mathbb{R}^{n_g}`` | `g([t, ]⋯[, v])`† | — | +| [`Data.Multiplier`](@ref CTBase.Data.Multiplier) | ``\mu : T^*\mathcal{X} \to \mathbb{R}^{n_g}`` | `μ([t, ]x, p[, v])` | — | + +The mathematical spaces are: + +- ``\mathcal{X} \subseteq \mathbb{R}^n`` — the **state space** (``n`` state dimensions). +- ``\mathcal{U} \subseteq \mathbb{R}^m`` — the **control space** (``m`` control dimensions). +- ``T^*\mathcal{X} = \mathcal{X} \times \mathbb{R}^n`` — the **cotangent bundle** (state + costate, ``2n`` dimensions). +- ``\mathbb{R}^{n_v}`` — the **optimisation variable** space (``n_v`` variable dimensions; absent when fixed). +- ``\mathbb{R}`` — scalars; ``\mathbb{R}^{n_g}`` — constraint values (``n_g`` constraint dimensions). +- ``\cdots`` — the domain depends on an additional trait (feedback or constraint-kind). + +In the call patterns, brackets `[…]` denote optional arguments controlled by traits: + +- `t` is present when `is_autonomous=false` (non-autonomous). +- `v` is present when `is_variable=true` (non-fixed). +- † `⋯` depends on an additional trait: **feedback** for `ControlLaw` (see below), + **constraint-kind** for `PathConstraint` (see below). + +All ten share the same time-dependence and variable-dependence trait axes. +`VectorField` and `HamiltonianVectorField` also carry a mutability trait (in-place / out-of-place); `ControlLaw` carries a feedback trait; `PathConstraint` carries a constraint-kind trait (see [Traits](traits.md)). @@ -40,9 +56,15 @@ All ten share the same trait axes — time dependence and variable dependence. ## VectorField -A `VectorField` wraps a Julia function representing ``\dot{x} = X(\cdots)``. +A `VectorField` wraps a Julia function representing the dynamics ``\dot{x} = X(t, x, v)``. + +The **natural** out-of-place call is `X([t, ]x[, v])`, where `t` and `v` are +optional arguments controlled by the time-dependence and variable-dependence traits. +A **uniform** call `X(t, x, v)` always works regardless of traits (unused arguments +are ignored). In-place is supported: the derivative buffer `dx` is prepended, +giving `X!(dx, [t, ]x[, v])`. -### Construction +**Construction** ```@example data using CTBase.Data, CTBase.Traits @@ -65,7 +87,7 @@ vf1 The `show` output summarises the traits and both call signatures (natural and uniform). -### In-place construction +**In-place construction** Prefer out-of-place for clarity. Use in-place when avoiding allocations matters: @@ -85,7 +107,7 @@ f_multi(dx, x) = (dx .= -x; nothing) vf_explicit = Data.VectorField(f_multi; is_inplace=true) ``` -### Calling +**Calling** ```@example data x0 = [1.0, 0.5] @@ -99,13 +121,18 @@ vf2(0.5, x0) # non-autonomous natural call ## Hamiltonian -A `Hamiltonian` wraps a scalar function ``H(x, p) \in \mathbb{R}``. +A `Hamiltonian` wraps a scalar function ``H(t, x, p, v) \in \mathbb{R}``. ```math -H : T^*\mathcal{X} \to \mathbb{R}, \quad (x, p) \mapsto H(x, p). +H : \mathbb{R} \times T^*\mathcal{X} \times \mathbb{R}^{n_v} \to \mathbb{R}, \quad (t, x, p, v) \mapsto H(t, x, p, v). ``` -### Construction +The **natural** call is `H([t, ]x, p[, v])`, where `t` and `v` are optional +arguments controlled by the time-dependence and variable-dependence traits. +A **uniform** call `H(t, x, p, v)` always works regardless of traits. +In-place is not supported (scalar return value). + +**Construction** ```@example data # Autonomous, fixed (default): H(x, p) @@ -120,7 +147,7 @@ h3 = Data.Hamiltonian((x, p, v) -> dot(p, x) * v[1]; is_variable=true) h1 ``` -### Calling +**Calling** ```@example data x0, p0 = [1.0, 0.5], [0.3, 0.7] @@ -139,14 +166,20 @@ A `HamiltonianVectorField` wraps the map when the derivatives are provided **explicitly** (no automatic differentiation). ```math -\vec{H}(x, p) = \bigl(\partial_p H(x,p),\; -\partial_x H(x,p)\bigr). +\vec{H}(t, x, p, v) = \bigl(\partial_p H,\; -\partial_x H\bigr). ``` +The **natural** out-of-place call is `HVF([t, ]x, p[, v])`, where `t` and `v` are +optional arguments controlled by the time-dependence and variable-dependence traits. +A **uniform** call `HVF(t, x, p, v)` always works regardless of traits. +In-place is supported: the derivative buffers `dx, dp` are prepended, +giving `HVF!(dx, dp, [t, ]x, p[, v])`. + Use this when the Hamiltonian equations are known analytically. When only the scalar Hamiltonian is available and the derivatives must be obtained by automatic differentiation, use [`Data.Hamiltonian`](@ref CTBase.Data.Hamiltonian) instead. -### Construction +**Construction** ```@example data # Harmonic oscillator H = (x²+p²)/2: ẋ = p, ṗ = -x @@ -158,14 +191,14 @@ hvf_na = Data.HamiltonianVectorField((t, x, p) -> (p, -x .* t); is_autonomous=fa hvf ``` -### Calling +**Calling** ```@example data dx, dp = hvf(x0, p0) # natural call: returns (ẋ, ṗ) (dx, dp) ``` -### The `variable_costate` keyword +**The `variable_costate` keyword** For **non-fixed** Hamiltonian vector fields, the call accepts an optional `variable_costate` keyword (default `false`). @@ -215,20 +248,25 @@ For a plain user-supplied function that does not accept `variable_costate`, pass ## PseudoHamiltonian -A `PseudoHamiltonian` wraps a scalar function ``\tilde{H}(x, p, u) \in \mathbb{R}`` +A `PseudoHamiltonian` wraps a scalar function ``\tilde{H}(t, x, p, u, v) \in \mathbb{R}`` that extends the standard Hamiltonian with an explicit control argument ``u``. ```math -\tilde{H} : T^*\mathcal{X} \times \mathcal{U} \to \mathbb{R}, \quad (x, p, u) \mapsto \tilde{H}(x, p, u). +\tilde{H} : \mathbb{R} \times T^*\mathcal{X} \times \mathcal{U} \times \mathbb{R}^{n_v} \to \mathbb{R}, \quad (t, x, p, u, v) \mapsto \tilde{H}(t, x, p, u, v). ``` +The **natural** call is `H̃([t, ]x, p, u[, v])`, where `t` and `v` are optional +arguments controlled by the time-dependence and variable-dependence traits. +A **uniform** call `H̃(t, x, p, u, v)` always works regardless of traits. +In-place is not supported (scalar return value). + Unlike [`Data.Hamiltonian`](@ref CTBase.Data.Hamiltonian), which encodes the control implicitly, a pseudo-Hamiltonian takes the control as an additional argument. This enables dynamic closed-loop flows where the control is computed from the pseudo-Hamiltonian's maximisation condition (PMP stationarity: ``\partial \tilde{H} / \partial u = 0``). -### Construction +**Construction** ```@example data # Autonomous, fixed (default): H̃(x, p, u) @@ -243,7 +281,7 @@ ph3 = Data.PseudoHamiltonian((x, p, u, v) -> dot(p, x) + u^2 + v[1]; is_variable ph1 ``` -### Calling +**Calling** ```@example data x0, p0, u0 = [1.0, 0.5], [0.3, 0.7], 2.0 @@ -269,10 +307,16 @@ dynamic closed-loop [`Data.ControlLaw`](@ref CTBase.Data.ControlLaw): H(t, x, p, v) = \tilde{H}(t, x, p, u(t, x, p, v), v). ``` +The **natural** call is `H([t, ]x, p[, v])`, where the time/variable dependences +are the **join** of the pseudo-Hamiltonian and the control law — +`NonAutonomous`/`NonFixed` win. +A **uniform** call `H(t, x, p, v)` always works regardless of traits. +In-place is not supported (scalar return value). + It subtypes [`Data.AbstractHamiltonian`](@ref CTBase.Data.AbstractHamiltonian), so it *is* a Hamiltonian and can be used anywhere one is expected. -### Construction +**Construction** ```@example data # Pseudo-Hamiltonian H̃(x, p, u) = p⋅x + u² (scalar state/costate/control) @@ -291,7 +335,7 @@ The composed time/variable dependences are the **join** of the two inputs — `NonAutonomous`/`NonFixed` win. A time-varying feedback on an autonomous pseudo-Hamiltonian correctly yields a `NonAutonomous` composed Hamiltonian. -### Calling +**Calling** ```@example data x0, p0 = 1.0, 0.5 @@ -300,7 +344,7 @@ H(x0, p0) # natural call H(0.0, x0, p0, nothing) # uniform call ``` -### Getters +**Getters** ```@example data Data.pseudo_hamiltonian(H) # the underlying PseudoHamiltonian @@ -315,18 +359,19 @@ The constructor rejects non-`DynClosedLoop` laws (`OpenLoop`, `ClosedLoop`) with ## ControlLaw A `ControlLaw` wraps a function ``u(\cdots)`` that provides the control input for -an optimal control problem. The feedback trait (see [Traits](traits.md)) -determines which arguments the control law depends on. +an optimal control problem. The **feedback** trait (see [Traits](traits.md)) +determines which primal variables the control law depends on, and the +time/variable traits add `t` and `v` as for other data types. Three user-facing constructors fix the feedback trait: -| Constructor | Feedback trait | Natural signature (autonomous/fixed) | +| Constructor | Feedback trait | Out-of-place call | |---|---|---| -| [`Data.OpenLoop`](@ref CTBase.Data.OpenLoop) | `OpenLoopFeedback` | `u()` | -| [`Data.ClosedLoop`](@ref CTBase.Data.ClosedLoop) | `ClosedLoopFeedback` | `u(x)` | -| [`Data.DynClosedLoop`](@ref CTBase.Data.DynClosedLoop) | `DynClosedLoopFeedback` | `u(x, p)` | +| [`Data.OpenLoop`](@ref CTBase.Data.OpenLoop) | `OpenLoopFeedback` | `u([t][, v])` | +| [`Data.ClosedLoop`](@ref CTBase.Data.ClosedLoop) | `ClosedLoopFeedback` | `u([t, ]x[, v])` | +| [`Data.DynClosedLoop`](@ref CTBase.Data.DynClosedLoop) | `DynClosedLoopFeedback` | `u([t, ]x, p[, v])` | -### Construction +**Construction** ```@example data # Open-loop, autonomous, fixed (default): u() @@ -348,16 +393,16 @@ other data types: u_ol_na = Data.OpenLoop((t, v) -> t * v; is_autonomous=false, is_variable=true) ``` -### Calling +**Calling** Every `ControlLaw` is callable via its **natural** signature (matching the traits) and via a **uniform** signature that depends on the feedback trait: -| Feedback | Natural `(Aut, Fixed)` | Uniform | +| Feedback | Natural | Uniform | |---|---|---| -| `OpenLoop` | `u()` | `u(t, v)` | -| `ClosedLoop` | `u(x)` | `u(t, x, v)` | -| `DynClosedLoop` | `u(x, p)` | `u(t, x, p, v)` | +| `OpenLoop` | `u([t][, v])` | `u(t, v)` | +| `ClosedLoop` | `u([t, ]x[, v])` | `u(t, x, v)` | +| `DynClosedLoop` | `u([t, ]x, p[, v])` | `u(t, x, p, v)` | ```@example data u_ol() # natural call @@ -381,17 +426,18 @@ while dynamic closed-loop control laws carry A `PathConstraint` wraps a function ``g(\cdots)`` that evaluates a path constraint along the trajectory. The **constraint-kind** trait (see [Traits](traits.md)) -determines which primal variables the constraint depends on. +determines which primal variables the constraint depends on, and the +time/variable traits add `t` and `v` as for other data types. Three user-facing constructors fix the constraint kind: -| Constructor | Constraint kind | Natural signature (autonomous/fixed) | +| Constructor | Constraint kind | Out-of-place call | |---|---|---| -| [`Data.StateConstraint`](@ref CTBase.Data.StateConstraint) | `StateConstraintKind` | `g(x)` | -| [`Data.ControlConstraint`](@ref CTBase.Data.ControlConstraint) | `ControlConstraintKind` | `g(u)` | -| [`Data.MixedConstraint`](@ref CTBase.Data.MixedConstraint) | `MixedConstraintKind` | `g(x, u)` | +| [`Data.StateConstraint`](@ref CTBase.Data.StateConstraint) | `StateConstraintKind` | `g([t, ]x[, v])` | +| [`Data.ControlConstraint`](@ref CTBase.Data.ControlConstraint) | `ControlConstraintKind` | `g([t, ]u[, v])` | +| [`Data.MixedConstraint`](@ref CTBase.Data.MixedConstraint) | `MixedConstraintKind` | `g([t, ]x, u[, v])` | -### Construction +**Construction** ```@example data # State constraint, autonomous, fixed (default): g(x) @@ -409,7 +455,7 @@ g_state_na = Data.StateConstraint((t, x) -> t * x[1]; is_autonomous=false) g_state ``` -### Calling +**Calling** Every `PathConstraint` is callable via its **natural** signature (matching the traits) and via a **uniform** signature `g(t, x, u, v)` that ignores unused @@ -438,12 +484,19 @@ Traits.is_state_constraint(g_state), Traits.is_control_constraint(g_ctrl), Trait ## Multiplier -A `Multiplier` wraps a function ``\mu(t, x, p[, v])`` returning the Lagrange -multiplier associated with a path constraint. It has the same call structure as a +A `Multiplier` wraps a function ``\mu(t, x, p, v)`` returning the Lagrange +multiplier associated with a path constraint. + +The **natural** call is `μ([t, ]x, p[, v])`, where `t` and `v` are optional +arguments controlled by the time-dependence and variable-dependence traits. +A **uniform** call `μ(t, x, p, v)` always works regardless of traits. +In-place is not supported. + +It has the same call structure as a [`Data.Hamiltonian`](@ref CTBase.Data.Hamiltonian) — it depends on the state and costate — but carries no dynamics semantics of its own. -### Construction +**Construction** ```@example data # Autonomous, fixed (default): μ(x, p) @@ -455,7 +508,7 @@ costate — but carries no dynamics semantics of its own. μ1 ``` -### Calling +**Calling** ```@example data x0, p0 = [1.0, 0.5], [0.3, 0.7] @@ -469,17 +522,20 @@ x0, p0 = [1.0, 0.5], [0.3, 0.7] ## ControlledVectorField -A `ControlledVectorField` wraps a function ``f_c(t, x, u[, v])`` that returns the +A `ControlledVectorField` wraps a function ``f_c(t, x, u, v)`` that returns the state derivative with an **explicit control argument** ``u``. It is the state-space analogue of [`Data.PseudoHamiltonian`](@ref CTBase.Data.PseudoHamiltonian): where a pseudo-Hamiltonian carries the control alongside the costate, a controlled vector field carries the control alongside the state. -Unlike [`Data.VectorField`](@ref CTBase.Data.VectorField), it is always -**out-of-place** (no mutability trait) and carries -[`Traits.StateDynamics`](@ref CTBase.Traits.StateDynamics). +The **natural** call is `fc([t, ]x, u[, v])`, where `t` and `v` are optional +arguments controlled by the time-dependence and variable-dependence traits. +A **uniform** call `fc(t, x, u, v)` always works regardless of traits. +In-place is not supported (always out-of-place). -### Construction +It carries [`Traits.StateDynamics`](@ref CTBase.Traits.StateDynamics). + +**Construction** ```@example data # Autonomous, fixed (default): fc(x, u) @@ -497,7 +553,7 @@ x0, u0 = 1.0, 2.0 fc1 ``` -### Calling +**Calling** ```@example data fc1(x0, u0) # natural call @@ -521,10 +577,16 @@ where the control is `u(t, v)` for an open-loop law and `u(t, x, v)` for a closed-loop law. It is the state-space analogue of [`Data.ComposedHamiltonian`](@ref CTBase.Data.ComposedHamiltonian). +The **natural** call is `g([t, ]x[, v])`, where the time/variable dependences +are the **join** of the controlled vector field and the control law — +`NonAutonomous`/`NonFixed` win. +A **uniform** call `g(t, x, v)` always works regardless of traits. +In-place is not supported (always out-of-place). + It subtypes [`Data.AbstractVectorField`](@ref CTBase.Data.AbstractVectorField) with `OutOfPlace` mutability, so it *is* a vector field usable anywhere one is expected. -### Construction +**Construction** ```@example data # Controlled vector field fc(x, u) = -x + u (scalar state/control) @@ -542,14 +604,14 @@ g The composed time/variable dependences are the **join** of the two inputs — `NonAutonomous`/`NonFixed` win. -### Calling +**Calling** ```@example data g(3.0) # natural call: g(x) = -2x g(0.0, 3.0, nothing) # uniform call (t, x, v) ``` -### Getters +**Getters** ```@example data Data.controlled_vector_field(g) # the underlying ControlledVectorField diff --git a/docs/src/guide/descriptions.md b/docs/src/guide/descriptions.md index 42bbf528..275ae70d 100644 --- a/docs/src/guide/descriptions.md +++ b/docs/src/guide/descriptions.md @@ -24,15 +24,21 @@ string-free, and composable way. ```@repl desc algorithms = () -algorithms = CTBase.Descriptions.add(algorithms, (:descent, :bfgs, :bisection)) -algorithms = CTBase.Descriptions.add(algorithms, (:descent, :gradient, :fixedstep)) +algorithms = CTBase.Descriptions.add( + algorithms, (:descent, :bfgs, :bisection), +) +algorithms = CTBase.Descriptions.add( + algorithms, (:descent, :gradient, :fixedstep), +) ``` Attempting to add a duplicate raises [`CTBase.Exceptions.IncorrectArgument`](@ref): ```@repl desc try # hide -CTBase.Descriptions.add(algorithms, (:descent, :bfgs, :bisection)) +CTBase.Descriptions.add( + algorithms, (:descent, :bfgs, :bisection), +) catch e # hide showerror(IOContext(stdout, :color => false), e) # hide end # hide @@ -44,8 +50,12 @@ end # hide all provided symbols: ```@repl desc -CTBase.Descriptions.complete(:bisection; descriptions=algorithms) -CTBase.Descriptions.complete(:gradient, :fixedstep; descriptions=algorithms) +CTBase.Descriptions.complete( + :bisection; descriptions=algorithms, +) +CTBase.Descriptions.complete( + :gradient, :fixedstep; descriptions=algorithms, +) ``` Among all entries that are a superset of the requested symbols, the one with the @@ -54,7 +64,9 @@ largest overlap is returned (first wins on tie). If no entry matches, ```@repl desc try # hide -CTBase.Descriptions.complete(:euler; descriptions=algorithms) +CTBase.Descriptions.complete( + :euler; descriptions=algorithms, +) catch e # hide showerror(IOContext(stdout, :color => false), e) # hide end # hide @@ -66,7 +78,9 @@ end # hide removed — useful for extracting the remainder after stripping a known prefix: ```@repl desc -full = CTBase.Descriptions.complete(:bisection; descriptions=algorithms) +full = CTBase.Descriptions.complete( + :bisection; descriptions=algorithms, +) CTBase.Descriptions.remove(full, (:descent, :bfgs)) ``` @@ -74,8 +88,8 @@ CTBase.Descriptions.remove(full, (:descent, :bfgs)) | Function | Purpose | Throws | | :--- | :--- | :--- | -| [`CTBase.Descriptions.add`](@ref) | Add a description to a catalogue | [`CTBase.Exceptions.IncorrectArgument`](@ref) on duplicate | -| [`CTBase.Descriptions.complete`](@ref) | Complete a partial description | [`CTBase.Exceptions.AmbiguousDescription`](@ref) on no/ambiguous match | +| [`CTBase.Descriptions.add`](@ref) | Add a description to a catalogue | [`IncorrectArgument`](@ref CTBase.Exceptions.IncorrectArgument) on duplicate | +| [`CTBase.Descriptions.complete`](@ref) | Complete a partial description | [`AmbiguousDescription`](@ref CTBase.Exceptions.AmbiguousDescription) on no match | | [`CTBase.Descriptions.remove`](@ref) | Remove symbols from a description | — | ## See Also diff --git a/docs/src/guide/differentiation.md b/docs/src/guide/differentiation.md index 0ea5b094..b54a5778 100644 --- a/docs/src/guide/differentiation.md +++ b/docs/src/guide/differentiation.md @@ -117,8 +117,14 @@ argument comes first, the frozen ones follow in slot order: H(x, p) = 0.5 * sum(p .^ 2) + sum(x .^ 2) x = [1.0, 2.0]; p = [3.0, 4.0] -∂x = Differentiation.differentiate(backend, H, Val(1), x, p) # active = x (slot 1), const = p -∂p = Differentiation.differentiate(backend, H, Val(2), p, x) # active = p (slot 2), const = x +# active = x (slot 1), const = p +∂x = Differentiation.differentiate( + backend, H, Val(1), x, p, +) +# active = p (slot 2), const = x +∂p = Differentiation.differentiate( + backend, H, Val(2), p, x, +) (∂x, ∂p) ``` @@ -152,7 +158,9 @@ Hamilton's equations (`ẋ = ∂H/∂p`, `ṗ = -∂H/∂x`): ```@example diff # H(x, p) = ½(‖x‖² + ‖p‖²) → ∂H/∂x = x, ∂H/∂p = p h = Data.Hamiltonian((x, p) -> 0.5 * (sum(abs2, x) + sum(abs2, p))) -∂x, ∂p = Differentiation.hamiltonian_gradient(backend, h, 0.0, [1.0, 2.0], [3.0, 4.0], nothing) +∂x, ∂p = Differentiation.hamiltonian_gradient( + backend, h, 0.0, [1.0, 2.0], [3.0, 4.0], nothing, +) (∂x, ∂p) ``` @@ -162,8 +170,13 @@ returns `∂H/∂v`: ```@example diff # H(x, p, v) = ½(‖x‖² + ‖p‖² + ‖v‖²) → ∂H/∂v = v -hv = Data.Hamiltonian((x, p, v) -> 0.5 * (sum(abs2, x) + sum(abs2, p) + sum(abs2, v)); is_variable=true) -Differentiation.variable_gradient(backend, hv, 0.0, [1.0, 2.0], [3.0, 4.0], [5.0, 6.0]) +hv = Data.Hamiltonian( + (x, p, v) -> 0.5 * (sum(abs2, x) + sum(abs2, p) + sum(abs2, v)); + is_variable=true, +) +Differentiation.variable_gradient( + backend, hv, 0.0, [1.0, 2.0], [3.0, 4.0], [5.0, 6.0], +) ``` ## Pseudo-Hamiltonian gradients @@ -177,8 +190,12 @@ Hamilton's equations: ```@example diff # H̃(x, p, u) = ½(‖x‖² + ‖p‖²) + u² → ∂H̃/∂x = x, ∂H̃/∂p = p -ph = Data.PseudoHamiltonian((x, p, u) -> 0.5 * (sum(abs2, x) + sum(abs2, p)) + u^2) -∂x, ∂p = Differentiation.pseudo_hamiltonian_gradient(backend, ph, 0.0, [1.0, 2.0], [3.0, 4.0], 2.0, nothing) +ph = Data.PseudoHamiltonian( + (x, p, u) -> 0.5 * (sum(abs2, x) + sum(abs2, p)) + u^2, +) +∂x, ∂p = Differentiation.pseudo_hamiltonian_gradient( + backend, ph, 0.0, [1.0, 2.0], [3.0, 4.0], 2.0, nothing, +) (∂x, ∂p) ``` @@ -188,14 +205,21 @@ Maximum Principle stationarity condition: ```@example diff # ∂H̃/∂u = 2u = 4.0 -Differentiation.pseudo_hamiltonian_control_gradient(backend, ph, 0.0, [1.0, 2.0], [3.0, 4.0], 2.0, nothing) +Differentiation.pseudo_hamiltonian_control_gradient( + backend, ph, 0.0, [1.0, 2.0], [3.0, 4.0], 2.0, nothing, +) ``` For a non-fixed pseudo-Hamiltonian, the `v` argument carries the variable: ```@example diff -phv = Data.PseudoHamiltonian((x, p, u, v) -> 0.5 * (sum(abs2, x) + sum(abs2, p)) + u^2 + v[1]; is_variable=true) -Differentiation.pseudo_hamiltonian_control_gradient(backend, phv, 0.0, [1.0, 2.0], [3.0, 4.0], 2.0, [5.0]) +phv = Data.PseudoHamiltonian( + (x, p, u, v) -> 0.5 * (sum(abs2, x) + sum(abs2, p)) + u^2 + v[1]; + is_variable=true, +) +Differentiation.pseudo_hamiltonian_control_gradient( + backend, phv, 0.0, [1.0, 2.0], [3.0, 4.0], 2.0, [5.0], +) ``` ## Pseudo-Hamiltonian variable gradient @@ -207,8 +231,13 @@ the total derivative `∂/∂v[H̃(t, x, p, u(t,x,p,v), v)]`: ```@example diff # H̃(x, p, u, v) = ½(‖x‖² + ‖p‖²) + u² + v[1] → ∂H̃/∂v = [1.0] -phv = Data.PseudoHamiltonian((x, p, u, v) -> 0.5 * (sum(abs2, x) + sum(abs2, p)) + u^2 + v[1]; is_variable=true) -Differentiation.pseudo_variable_gradient(backend, phv, 0.0, [1.0, 2.0], [3.0, 4.0], 2.0, [5.0]) +phv = Data.PseudoHamiltonian( + (x, p, u, v) -> 0.5 * (sum(abs2, x) + sum(abs2, p)) + u^2 + v[1]; + is_variable=true, +) +Differentiation.pseudo_variable_gradient( + backend, phv, 0.0, [1.0, 2.0], [3.0, 4.0], 2.0, [5.0], +) ``` This differs from [`Differentiation.variable_gradient`](@ref CTBase.Differentiation.variable_gradient) @@ -227,7 +256,9 @@ struct MyBackend <: Differentiation.AbstractADBackend options::Strategies.StrategyOptions end try # hide -Differentiation.gradient(MyBackend(Strategies.StrategyOptions()), x -> sum(x), [1.0]) +Differentiation.gradient( + MyBackend(Strategies.StrategyOptions()), x -> sum(x), [1.0], +) catch e # hide showerror(IOContext(stdout, :color => false), e) # hide end # hide diff --git a/docs/src/guide/exceptions.md b/docs/src/guide/exceptions.md index 363d090d..ac06a044 100644 --- a/docs/src/guide/exceptions.md +++ b/docs/src/guide/exceptions.md @@ -298,7 +298,11 @@ The enriched display automatically suggests: ```@repl using CTBase try # hide -throw(CTBase.Exceptions.ExtensionError(:Plots, feature="result visualization", context="plot_results function")) +throw(CTBase.Exceptions.ExtensionError( + :Plots, + feature="result visualization", + context="plot_results function", +)) catch e # hide showerror(IOContext(stdout, :color => false), e) # hide end # hide @@ -351,7 +355,12 @@ The enriched display shows the solver-specific return code: ```@repl solver-failure try # hide -throw(CTBase.Exceptions.SolverFailure("ODE integration failed", retcode=":Unstable", suggestion="Reduce time step or check initial conditions", context="SciML integrator")) +throw(CTBase.Exceptions.SolverFailure( + "ODE integration failed", + retcode=":Unstable", + suggestion="Reduce time step or check initial conditions", + context="SciML integrator", +)) catch e # hide showerror(IOContext(stdout, :color => false), e) # hide end # hide @@ -380,13 +389,13 @@ end # hide | Situation | Exception | Example | |-----------|-----------|---------| -| Invalid argument value | [`CTBase.Exceptions.IncorrectArgument`](@ref) | `throw(IncorrectArgument("x must be > 0", got="-5", expected="> 0"))` | -| Wrong function call order | [`CTBase.Exceptions.PreconditionError`](@ref) | `throw(PreconditionError("Must initialize before configure"))` | -| Unimplemented interface | [`CTBase.Exceptions.NotImplemented`](@ref) | `throw(NotImplemented("run! not implemented for MyType"))` | -| Parsing error | [`CTBase.Exceptions.ParsingError`](@ref) | `throw(ParsingError("unexpected token", location="line 10"))` | -| Ambiguous description | [`CTBase.Exceptions.AmbiguousDescription`](@ref) | `throw(AmbiguousDescription((:x,), candidates=["(:a,:b)", "(:c,:d)"]))` | -| Missing optional dependency | [`CTBase.Exceptions.ExtensionError`](@ref) | `throw(ExtensionError(:Plots, feature="plotting"))` | -| Solver/integrator failure | [`CTBase.Exceptions.SolverFailure`](@ref) | `throw(SolverFailure("ODE failed", retcode=":Unstable"))` | +| Invalid argument value | [`CTBase.Exceptions.IncorrectArgument`](@ref) | `throw(IncorrectArgument("x must be > 0", got="-5"))` | +| Wrong function call order | [`CTBase.Exceptions.PreconditionError`](@ref) | `throw(PreconditionError("Must init first"))` | +| Unimplemented interface | [`CTBase.Exceptions.NotImplemented`](@ref) | `throw(NotImplemented("run! not implemented"))` | +| Parsing error | [`CTBase.Exceptions.ParsingError`](@ref) | `throw(ParsingError("unexpected token"))` | +| Ambiguous description | [`CTBase.Exceptions.AmbiguousDescription`](@ref) | `throw(AmbiguousDescription((:x,)))` | +| Missing optional dependency | [`CTBase.Exceptions.ExtensionError`](@ref) | `throw(ExtensionError(:Plots))` | +| Solver/integrator failure | [`CTBase.Exceptions.SolverFailure`](@ref) | `throw(SolverFailure("ODE failed"))` | ## Enriched Error Display diff --git a/docs/src/guide/implementing-a-strategy.md b/docs/src/guide/implementing-a-strategy.md index 7f119eef..d30ff157 100644 --- a/docs/src/guide/implementing-a-strategy.md +++ b/docs/src/guide/implementing-a-strategy.md @@ -21,8 +21,8 @@ Every strategy implements a **two-level contract** that separates static metadat ```text Type-Level (no instantiation needed) -├─ id(::Type{<:S}) → Symbol (routing, registry lookup) -└─ metadata(::Type{<:S}) → StrategyMetadata (option specs + validation rules) +├─ id(::Type{<:S}) → Symbol (routing, registry lookup) +└─ metadata(::Type{<:S}) → StrategyMetadata (option specs + validation rules) │ ▼ routing, validation Constructor(; mode, kwargs...) @@ -121,7 +121,9 @@ The constructor uses `build_strategy_options` to validate and merge user-provide ```@example strategy function Collocation(; mode::Symbol = :strict, kwargs...) - opts = Strategies.build_strategy_options(Collocation; mode = mode, kwargs...) + opts = Strategies.build_strategy_options( + Collocation; mode = mode, kwargs..., + ) return Collocation(opts) end nothing # hide @@ -227,7 +229,9 @@ function Strategies.metadata(::Type{<:DirectShooting}) end function DirectShooting(; mode::Symbol = :strict, kwargs...) - opts = Strategies.build_strategy_options(DirectShooting; mode = mode, kwargs...) + opts = Strategies.build_strategy_options( + DirectShooting; mode = mode, kwargs..., + ) return DirectShooting(opts) end @@ -259,21 +263,34 @@ registry = Strategies.create_registry( Query the registry: ```@repl strategy -Strategies.strategy_ids(AbstractOptimalControlDiscretizer, registry) +Strategies.strategy_ids( + AbstractOptimalControlDiscretizer, registry, +) ``` ```@repl strategy -Strategies.type_from_id(:collocation, AbstractOptimalControlDiscretizer, registry) +Strategies.type_from_id( + :collocation, + AbstractOptimalControlDiscretizer, registry, +) ``` Build a strategy from the registry: ```@example strategy -Strategies.build_strategy(:collocation, AbstractOptimalControlDiscretizer, registry; grid_size = 300) +Strategies.build_strategy( + :collocation, + AbstractOptimalControlDiscretizer, registry; + grid_size = 300, +) ``` ```@example strategy -Strategies.build_strategy(:direct_shooting, AbstractOptimalControlDiscretizer, registry; grid_size = 50) +Strategies.build_strategy( + :direct_shooting, + AbstractOptimalControlDiscretizer, registry; + grid_size = 50, +) ``` ## Integration with Method Tuples @@ -282,14 +299,21 @@ In the full CTBase pipeline, a **method tuple** like `(:collocation, :adnlp, :ip ```@repl strategy method = (:collocation, :adnlp, :ipopt) -Strategies.extract_id_from_method(method, AbstractOptimalControlDiscretizer, registry) +Strategies.extract_id_from_method( + method, AbstractOptimalControlDiscretizer, registry, +) ``` Build a strategy directly from a method tuple: ```@example strategy -id = Strategies.extract_id_from_method(method, AbstractOptimalControlDiscretizer, registry) -Strategies.build_strategy(id, AbstractOptimalControlDiscretizer, registry; grid_size = 500, scheme = :trapeze) +id = Strategies.extract_id_from_method( + method, AbstractOptimalControlDiscretizer, registry, +) +Strategies.build_strategy( + id, AbstractOptimalControlDiscretizer, registry; + grid_size = 500, scheme = :trapeze, +) ``` See [Orchestration & Routing](@ref) for the full multi-strategy routing system. @@ -319,7 +343,9 @@ Strategies.option_type(Collocation, :scheme) ``` ```@repl strategy -Strategies.option_description(Collocation, :grid_size) +Strategies.option_description( + Collocation, :grid_size, +) ``` ## Advanced Patterns @@ -329,7 +355,11 @@ Strategies.option_description(Collocation, :grid_size) Use `mode = :permissive` to accept backend-specific options that are not declared in the metadata: ```@example strategy -Collocation(grid_size = 500, custom_backend_param = 42; mode = :permissive) +Collocation( + grid_size = 500, + custom_backend_param = 42; + mode = :permissive, +) ``` Unknown options are stored with `:user` source but bypass type validation. Known options are still fully validated. @@ -341,7 +371,10 @@ Use `bypass(val)` (or its alias `force(val)`) to skip validation for a **single **Unknown option** — accepted silently, no warning: ```@example strategy -Collocation(grid_size = 500, custom_backend_param = bypass(42)) +Collocation( + grid_size = 500, + custom_backend_param = bypass(42), +) ``` **Known option with wrong type** — normally rejected, accepted with `bypass`: @@ -353,7 +386,7 @@ catch e; showerror(IOContext(stdout, :color => false), e) end # hide ``` ```@example strategy -Collocation(grid_size = bypass("oops")) # no error: validation skipped +Collocation(grid_size = bypass("oops")) # no error ``` This is more surgical than `mode = :permissive`: @@ -366,7 +399,7 @@ This is more surgical than `mode = :permissive`: `force` is an alias for `bypass` — choose the name that fits your mental model: ```julia -Collocation(grid_size = force("oops")) # same as bypass("oops") +Collocation(grid_size = force("oops")) # same as bypass ``` !!! warning "Use with care" @@ -386,7 +419,7 @@ Options.OptionDefinition( ) ``` -With this definition, `Collocation(N = 100)` would be equivalent to `Collocation(grid_size = 100)`. +With this definition, `Collocation(N = 100)` is equivalent to `Collocation(grid_size = 100)`. ### Custom Validators @@ -398,7 +431,9 @@ Options.OptionDefinition( type = Int, default = 250, description = "Number of time steps", - validator = x -> x > 0 || throw(ArgumentError("grid_size must be positive")), + validator = x -> x > 0 || throw( + ArgumentError("grid_size must be positive"), + ), ) ``` diff --git a/docs/src/guide/options-system.md b/docs/src/guide/options-system.md index 72fc2fe9..5e0a07a6 100644 --- a/docs/src/guide/options-system.md +++ b/docs/src/guide/options-system.md @@ -38,9 +38,11 @@ def = OptionDefinition( default = 1000, description = "Maximum number of iterations", aliases = (:maxiter,), - validator = x -> x >= 0 || throw(CTBase.Exceptions.IncorrectArgument( - "Invalid max_iter", got = "$x", expected = ">= 0", - )), + validator = x -> x >= 0 || throw( + CTBase.Exceptions.IncorrectArgument( + "Invalid max_iter", got = "$x", expected = ">= 0", + ), + ), ) ``` @@ -67,7 +69,10 @@ Type mismatch in the constructor: ```@repl options try # hide -OptionDefinition(name = :count, type = Integer, default = "hello", description = "A count") +OptionDefinition( + name = :count, type = Integer, + default = "hello", description = "A count", +) catch e # hide showerror(IOContext(stdout, :color => false), e) # hide end # hide @@ -80,7 +85,8 @@ Aliases allow users to use alternative names for the same option: ```@example options def_alias = OptionDefinition( name = :max_iter, type = Int, default = 100, - description = "Max iterations", aliases = (:maxiter, :max), + description = "Max iterations", + aliases = (:maxiter, :max), ) all_names(def_alias) ``` @@ -95,11 +101,14 @@ Validators follow the pattern `x -> condition || throw(...)`. They should return validated_def = OptionDefinition( name = :tol, type = Real, default = 1e-8, description = "Tolerance", - validator = x -> x > 0 || throw(CTBase.Exceptions.IncorrectArgument( - "Invalid tolerance", - got = "tol=$x", expected = "positive real number (> 0)", - suggestion = "Use 1e-6 or 1e-8", - )), + validator = x -> x > 0 || throw( + CTBase.Exceptions.IncorrectArgument( + "Invalid tolerance", + got = "tol=$x", + expected = "positive real number (> 0)", + suggestion = "Use 1e-6 or 1e-8", + ), + ), ) nothing # hide ``` @@ -123,9 +132,9 @@ NotProvided ``` ```@example options -# Option with NotProvided default — omitted if user doesn't provide it opt_np = OptionDefinition( - name = :mu_init, type = Real, default = NotProvided, + name = :mu_init, type = Real, + default = NotProvided, description = "Initial barrier parameter", ) ``` @@ -217,9 +226,18 @@ Options.is_computed(opt2) ```@example options meta = CTBase.Strategies.StrategyMetadata( - OptionDefinition(name = :tol, type = Real, default = 1e-8, description = "Tolerance"), - OptionDefinition(name = :max_iter, type = Integer, default = 1000, description = "Max iterations"), - OptionDefinition(name = :verbose, type = Bool, default = false, description = "Verbose output"), + OptionDefinition( + name = :tol, type = Real, + default = 1e-8, description = "Tolerance", + ), + OptionDefinition( + name = :max_iter, type = Integer, + default = 1000, description = "Max iterations", + ), + OptionDefinition( + name = :verbose, type = Bool, + default = false, description = "Verbose output", + ), ) ``` @@ -253,8 +271,8 @@ nothing # hide ``` ```@example options -opts = CTBase.Strategies.build_strategy_options(DemoStrategy; - max_iter = 500, tol = 1e-6, +opts = CTBase.Strategies.build_strategy_options( + DemoStrategy; max_iter = 500, tol = 1e-6, ) ``` @@ -347,8 +365,10 @@ end # hide Accepts unknown options with a warning and stores them with `:user` source: ```@example options -opts_perm = CTBase.Strategies.build_strategy_options(DemoStrategy; - mode = :permissive, max_iter = 500, custom_flag = true, +opts_perm = CTBase.Strategies.build_strategy_options( + DemoStrategy; + mode = :permissive, + max_iter = 500, custom_flag = true, ) println("keys: ", keys(opts_perm)) ``` @@ -364,7 +384,9 @@ def_grid = OptionDefinition( name = :grid_size, type = Int, default = 100, description = "Grid size", aliases = (:n,), ) -opt_value, remaining = extract_option((n = 200, tol = 1e-6), def_grid) +opt_value, remaining = extract_option( + (n = 200, tol = 1e-6), def_grid, +) println("Extracted: ", opt_value) println("Remaining: ", remaining) ``` @@ -393,10 +415,18 @@ Extracts multiple options at once: ```@example options defs = [ - OptionDefinition(name = :grid_size, type = Int, default = 100, description = "Grid"), - OptionDefinition(name = :tol, type = Float64, default = 1e-6, description = "Tol"), + OptionDefinition( + name = :grid_size, type = Int, + default = 100, description = "Grid", + ), + OptionDefinition( + name = :tol, type = Float64, + default = 1e-6, description = "Tol", + ), ] -extracted, remaining = extract_options((grid_size = 200, max_iter = 1000), defs) +extracted, remaining = extract_options( + (grid_size = 200, max_iter = 1000), defs, +) println("Extracted: ", extracted) println("Remaining: ", remaining) ``` diff --git a/docs/src/guide/orchestration-and-routing.md b/docs/src/guide/orchestration-and-routing.md index 77cc8942..5fc21815 100644 --- a/docs/src/guide/orchestration-and-routing.md +++ b/docs/src/guide/orchestration-and-routing.md @@ -14,39 +14,89 @@ using CTBase using CTBase.Options: OptionDefinition using CTBase.Strategies: route_to using CTBase.Orchestration: resolve_method, route_all_options -using CTBase.Orchestration: extract_strategy_ids, build_strategy_to_family_map, build_option_ownership_map +using CTBase.Orchestration: extract_strategy_ids +using CTBase.Orchestration: build_strategy_to_family_map +using CTBase.Orchestration: build_option_ownership_map ``` We define three fake strategies — a discretizer, a modeler, and a solver — with a shared `backend` option to demonstrate routing and disambiguation: ```@example routing # --- Fake discretizer family --- -abstract type AbstractFakeDiscretizer <: CTBase.Strategies.AbstractStrategy end -struct FakeCollocation <: AbstractFakeDiscretizer; options::CTBase.Strategies.StrategyOptions; end +abstract type AbstractFakeDiscretizer <: + CTBase.Strategies.AbstractStrategy end + +struct FakeCollocation <: AbstractFakeDiscretizer + options::CTBase.Strategies.StrategyOptions +end + CTBase.Strategies.id(::Type{<:FakeCollocation}) = :collocation -CTBase.Strategies.metadata(::Type{<:FakeCollocation}) = CTBase.Strategies.StrategyMetadata( - OptionDefinition(name = :grid_size, type = Int, default = 100, description = "Grid size"), + +CTBase.Strategies.metadata(::Type{<:FakeCollocation}) = + CTBase.Strategies.StrategyMetadata( + OptionDefinition( + name = :grid_size, type = Int, + default = 100, description = "Grid size", + ), + ) + +FakeCollocation(; kwargs...) = FakeCollocation( + CTBase.Strategies.build_strategy_options( + FakeCollocation; kwargs..., + ), ) -FakeCollocation(; kwargs...) = FakeCollocation(CTBase.Strategies.build_strategy_options(FakeCollocation; kwargs...)) # --- Fake modeler family --- -abstract type AbstractFakeModeler <: CTBase.Strategies.AbstractStrategy end -struct FakeADNLP <: AbstractFakeModeler; options::CTBase.Strategies.StrategyOptions; end +abstract type AbstractFakeModeler <: + CTBase.Strategies.AbstractStrategy end + +struct FakeADNLP <: AbstractFakeModeler + options::CTBase.Strategies.StrategyOptions +end + CTBase.Strategies.id(::Type{<:FakeADNLP}) = :adnlp -CTBase.Strategies.metadata(::Type{<:FakeADNLP}) = CTBase.Strategies.StrategyMetadata( - OptionDefinition(name = :backend, type = Symbol, default = :default, description = "AD backend"), + +CTBase.Strategies.metadata(::Type{<:FakeADNLP}) = + CTBase.Strategies.StrategyMetadata( + OptionDefinition( + name = :backend, type = Symbol, + default = :default, description = "AD backend", + ), + ) + +FakeADNLP(; kwargs...) = FakeADNLP( + CTBase.Strategies.build_strategy_options( + FakeADNLP; kwargs..., + ), ) -FakeADNLP(; kwargs...) = FakeADNLP(CTBase.Strategies.build_strategy_options(FakeADNLP; kwargs...)) # --- Fake solver family --- -abstract type AbstractFakeSolver <: CTBase.Strategies.AbstractStrategy end -struct FakeIpopt <: AbstractFakeSolver; options::CTBase.Strategies.StrategyOptions; end +abstract type AbstractFakeSolver <: + CTBase.Strategies.AbstractStrategy end + +struct FakeIpopt <: AbstractFakeSolver + options::CTBase.Strategies.StrategyOptions +end + CTBase.Strategies.id(::Type{<:FakeIpopt}) = :ipopt -CTBase.Strategies.metadata(::Type{<:FakeIpopt}) = CTBase.Strategies.StrategyMetadata( - OptionDefinition(name = :max_iter, type = Integer, default = 1000, description = "Max iterations"), - OptionDefinition(name = :backend, type = Symbol, default = :cpu, description = "Compute backend"), + +CTBase.Strategies.metadata(::Type{<:FakeIpopt}) = + CTBase.Strategies.StrategyMetadata( + OptionDefinition( + name = :max_iter, type = Integer, + default = 1000, description = "Max iterations", + ), + OptionDefinition( + name = :backend, type = Symbol, + default = :cpu, description = "Compute backend", + ), + ) + +FakeIpopt(; kwargs...) = FakeIpopt( + CTBase.Strategies.build_strategy_options( + FakeIpopt; kwargs..., + ), ) -FakeIpopt(; kwargs...) = FakeIpopt(CTBase.Strategies.build_strategy_options(FakeIpopt; kwargs...)) # --- Registry --- registry = CTBase.Strategies.create_registry( @@ -204,7 +254,9 @@ Note that `:backend` is owned by both `:modeler` and `:solver` — it is ambiguo Unpacks a `RoutedOption` into a vector of `(value, strategy_id)` pairs: ```@example routing -extract_strategy_ids(route_to(ipopt = 100, adnlp = 50), resolved) +extract_strategy_ids( + route_to(ipopt = 100, adnlp = 50), resolved, +) ``` For plain (non-routed) values, no disambiguation is detected — the function returns `nothing`: @@ -217,7 +269,9 @@ Passing an unknown strategy ID throws an error: ```@repl routing try # hide -extract_strategy_ids(route_to(unknown = 42), resolved) +extract_strategy_ids( + route_to(unknown = 42), resolved, +) catch e # hide showerror(IOContext(stdout, :color => false), e) # hide end # hide @@ -240,7 +294,10 @@ kwargs = ( display = false, # action option ) -routed = route_all_options(method, families, action_defs, kwargs, registry) +routed = route_all_options( + method, families, action_defs, + kwargs, registry, +) ``` Action options: @@ -259,7 +316,10 @@ routed.strategies ```@repl routing try # hide -route_all_options(method, families, action_defs, (foo = 42,), registry) +route_all_options( + method, families, action_defs, + (foo = 42,), registry, +) catch e # hide showerror(IOContext(stdout, :color => false), e) # hide end # hide @@ -269,7 +329,10 @@ end # hide ```@repl routing try # hide -route_all_options(method, families, action_defs, (backend = :sparse,), registry) +route_all_options( + method, families, action_defs, + (backend = :sparse,), registry, +) catch e # hide showerror(IOContext(stdout, :color => false), e) # hide end # hide diff --git a/docs/src/guide/strategy-parameters.md b/docs/src/guide/strategy-parameters.md index b2b1b38e..8eba9195 100644 --- a/docs/src/guide/strategy-parameters.md +++ b/docs/src/guide/strategy-parameters.md @@ -120,13 +120,16 @@ nothing # hide The dispatch is on `::Type{FakeOptimizer{P}}` — not on a second argument: ```@example params -function Strategies.metadata(::Type{FakeOptimizer{P}}) where {P <: Strategies.AbstractStrategyParameter} +function Strategies.metadata( + ::Type{FakeOptimizer{P}}, +) where {P <: Strategies.AbstractStrategyParameter} return Strategies.StrategyMetadata( Options.OptionDefinition( name = :precision, type = Symbol, default = __fake_default_precision(P), - description = "Numerical precision (:float64 for CPU, :float32 for GPU)", + description = "Numerical precision + (:float64 for CPU, :float32 for GPU)", computed = true, ), Options.OptionDefinition( @@ -158,8 +161,12 @@ Strategies.metadata(FakeOptimizer{Strategies.GPU}) The constructor is specialized on the parameterized type so `build_strategy_options` calls the right `metadata`: ```@example params -function FakeOptimizer{P}(; mode::Symbol = :strict, kwargs...) where {P <: Strategies.AbstractStrategyParameter} - opts = Strategies.build_strategy_options(FakeOptimizer{P}; mode = mode, kwargs...) +function FakeOptimizer{P}(; + mode::Symbol = :strict, kwargs..., +) where {P <: Strategies.AbstractStrategyParameter} + opts = Strategies.build_strategy_options( + FakeOptimizer{P}; mode = mode, kwargs..., + ) return FakeOptimizer{P}(opts) end nothing # hide @@ -188,7 +195,9 @@ solver = FakeOptimizer{Strategies.GPU}(max_iter = 200) ```@repl params solver[:precision] solver[:max_iter] -Strategies.source(Strategies.options(solver), :max_iter) +Strategies.source( + Strategies.options(solver), :max_iter, +) ``` ## Registering Parameterized Strategies @@ -210,11 +219,19 @@ Strategies.strategy_ids(AbstractFakeOptimizer, registry) ``` ```@example params -Strategies.type_from_id(:fake_optimizer, AbstractFakeOptimizer, registry; parameter=Strategies.CPU) +Strategies.type_from_id( + :fake_optimizer, + AbstractFakeOptimizer, registry; + parameter=Strategies.CPU, +) ``` ```@example params -Strategies.type_from_id(:fake_optimizer, AbstractFakeOptimizer, registry; parameter=Strategies.GPU) +Strategies.type_from_id( + :fake_optimizer, + AbstractFakeOptimizer, registry; + parameter=Strategies.GPU, +) ``` ## Building Strategies from the Registry @@ -222,12 +239,18 @@ Strategies.type_from_id(:fake_optimizer, AbstractFakeOptimizer, registry; parame `build_strategy` accepts an optional parameter type as second argument: ```@example params -Strategies.build_strategy(:fake_optimizer, Strategies.CPU, AbstractFakeOptimizer, registry; - max_iter = 300) +Strategies.build_strategy( + :fake_optimizer, Strategies.CPU, + AbstractFakeOptimizer, registry; + max_iter = 300, +) ``` ```@example params -Strategies.build_strategy(:fake_optimizer, Strategies.GPU, AbstractFakeOptimizer, registry) +Strategies.build_strategy( + :fake_optimizer, Strategies.GPU, + AbstractFakeOptimizer, registry, +) ``` ## Method Tuple Routing @@ -240,8 +263,12 @@ param = Strategies.extract_global_parameter_from_method(method, registry) ``` ```@example params -id = Strategies.extract_id_from_method(method, AbstractFakeOptimizer, registry) -Strategies.build_strategy(id, param, AbstractFakeOptimizer, registry) +id = Strategies.extract_id_from_method( + method, AbstractFakeOptimizer, registry, +) +Strategies.build_strategy( + id, param, AbstractFakeOptimizer, registry, +) ``` ## Mixed Registries @@ -265,7 +292,9 @@ function Strategies.metadata(::Type{<:FallbackOptimizer}) end function FallbackOptimizer(; mode::Symbol = :strict, kwargs...) - opts = Strategies.build_strategy_options(FallbackOptimizer; mode = mode, kwargs...) + opts = Strategies.build_strategy_options( + FallbackOptimizer; mode = mode, kwargs..., + ) return FallbackOptimizer(opts) end @@ -278,7 +307,9 @@ mixed_registry = Strategies.create_registry( ``` ```@example params -Strategies.strategy_ids(AbstractFakeOptimizer, mixed_registry) +Strategies.strategy_ids( + AbstractFakeOptimizer, mixed_registry, +) ``` ## `describe` with a Registry diff --git a/docs/src/guide/test-runner.md b/docs/src/guide/test-runner.md index 4affccbd..7c443d4d 100644 --- a/docs/src/guide/test-runner.md +++ b/docs/src/guide/test-runner.md @@ -54,8 +54,8 @@ const TEST_DIR = @__DIR__ CTBase.DevTools.run_tests(; args=String.(ARGS), # Pass command line arguments testset_name="MyPackage Tests", # Name of the main testset - available_tests=[ # List of available test groups/files - "suite/*" # Use glob pattern to include all tests in suite/ + available_tests=[ # Available test groups + "suite/*" # Glob: all tests in suite/ ], filename_builder = name -> "test_$(name).jl", funcname_builder = name -> "test_$(name)", @@ -110,7 +110,8 @@ julia --project -e 'using Pkg; Pkg.test("MyPackage")' ```bash julia --project -e 'using Pkg; Pkg.test("MyPackage"; test_args=["utils"])' -julia --project -e 'using Pkg; Pkg.test("MyPackage"; test_args=["core", "utils"])' +julia --project -e 'using Pkg; Pkg.test("MyPackage"; + test_args=["core", "utils"])' ``` ### Path prefix stripping @@ -120,7 +121,8 @@ Selection arguments starting with `test/` are **automatically stripped**, so the ```bash # These two commands run the same tests: julia --project -e 'using Pkg; Pkg.test("MyPackage"; test_args=["suite"])' -julia --project -e 'using Pkg; Pkg.test("MyPackage"; test_args=["test/suite"])' +julia --project -e 'using Pkg; Pkg.test("MyPackage"; + test_args=["test/suite"])' ``` This is convenient when tab-completing paths from the project root. @@ -245,12 +247,15 @@ The `on_test_start` and `on_test_done` callbacks allow custom actions during the struct TestRunInfo spec::Union{Symbol,String} # Test identifier filename::String # Absolute path of the test file - func_symbol::Union{Symbol,Nothing} # Function to call (nothing if eval_mode=false) - index::Int # 1-based index in the selected list + func_symbol::Union{Symbol,Nothing} # Function to call + # (nothing if eval_mode=false) + index::Int # 1-based index total::Int # Total number of selected tests status::Symbol # See below - error::Union{Exception,Nothing} # Captured exception (only when status == :error) - elapsed::Union{Float64,Nothing} # Wall-clock seconds (only in on_test_done) + error::Union{Exception,Nothing} # Captured exception + # (only when status == :error) + elapsed::Union{Float64,Nothing} # Wall-clock seconds + # (only in on_test_done) end ``` @@ -401,7 +406,8 @@ The progress bar detects `@test` failures by scanning the enclosing testset resu Run tests with verbose output to see detailed information: ```bash -julia --project -e 'using Pkg; Pkg.test("MyPackage"; test_args=["--verbose", "utils"])' +julia --project -e 'using Pkg; Pkg.test("MyPackage"; + test_args=["--verbose", "utils"])' ``` ## Best Practices From 349b420da10996953fd2ba1195b47eedb41680b2 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Sat, 11 Jul 2026 15:42:06 +0200 Subject: [PATCH 2/3] Add Base.show methods for Plotting IR types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add compact (one-line) and pretty (tree-style text/plain) display methods for Series, HLine, VLine, Axes, Leaf, HBox, VBox, Figure, and Panel, using the CTBase color palette via Core.get_format_codes. Features: - Recursive tree display with ├─/└─/│ indentation - Truncation beyond 5 children (… N more) - Pluralization (point/points, component/components) - Empty label filtering for Panel - Decorations (HLine/VLine) shown in Axes - Qualified imports per Handbook philosophy Add test/suite/plotting/test_display.jl with 120 tests covering compact, pretty, and integration (nested trees, truncation) cases. --- src/Plotting/Plotting.jl | 1 + src/Plotting/ir.jl | 309 ++++++++++++++++++++++++ src/Plotting/panel.jl | 41 ++++ test/suite/plotting/test_display.jl | 360 ++++++++++++++++++++++++++++ 4 files changed, 711 insertions(+) create mode 100644 test/suite/plotting/test_display.jl diff --git a/src/Plotting/Plotting.jl b/src/Plotting/Plotting.jl index b0d116ca..613774dc 100644 --- a/src/Plotting/Plotting.jl +++ b/src/Plotting/Plotting.jl @@ -41,6 +41,7 @@ module Plotting import DocStringExtensions: TYPEDEF, TYPEDSIGNATURES, TYPEDFIELDS import CTBase.Exceptions +import CTBase.Core include(joinpath(@__DIR__, "ir.jl")) include(joinpath(@__DIR__, "panel.jl")) diff --git a/src/Plotting/ir.jl b/src/Plotting/ir.jl index 31b74c31..5cab6fdf 100644 --- a/src/Plotting/ir.jl +++ b/src/Plotting/ir.jl @@ -281,3 +281,312 @@ $(TYPEDSIGNATURES) Return the number of drawable cells (leaves) in a [`Figure`](@ref). """ n_leaves(fig::Figure) = n_leaves(fig.root) + +# --- display ----------------------------------------------------------------- + +# Maximum number of children / series to show before truncating. +const _SHOW_LIMIT = 5 + +""" +$(TYPEDSIGNATURES) + +Compact one-line display of a [`Series`](@ref). + +See also: `Base.show` +""" +function Base.show(io::IO, s::Series) + fmt = Core.get_format_codes(io) + print(io, fmt.name, "Series", fmt.reset, "(") + print(io, fmt.label, repr(s.label), fmt.reset, ", ") + print(io, fmt.value, length(s.x), fmt.reset, " pts)") +end + +""" +$(TYPEDSIGNATURES) + +Pretty tree-style display of a [`Series`](@ref). + +See also: `Base.show` +""" +function Base.show(io::IO, ::MIME"text/plain", s::Series) + fmt = Core.get_format_codes(io) + np = length(s.x) + print(io, fmt.name, "Series", fmt.reset, " ") + print(io, fmt.label, repr(s.label), fmt.reset) + print(io, " (", fmt.value, np, fmt.reset, np == 1 ? " point)" : " points)") + if !isempty(s.style) + print(io, "\n style: ", fmt.value, s.style, fmt.reset) + end +end + +""" +$(TYPEDSIGNATURES) + +Compact one-line display of an [`HLine`](@ref). + +See also: `Base.show` +""" +function Base.show(io::IO, h::HLine) + fmt = Core.get_format_codes(io) + print(io, fmt.name, "HLine", fmt.reset, "(", fmt.value, h.value, fmt.reset, ")") +end + +""" +$(TYPEDSIGNATURES) + +Pretty display of an [`HLine`](@ref). + +See also: `Base.show` +""" +function Base.show(io::IO, ::MIME"text/plain", h::HLine) + fmt = Core.get_format_codes(io) + print(io, fmt.name, "HLine", fmt.reset, " at y=", fmt.value, h.value, fmt.reset) + if !isempty(h.style) + print(io, " style: ", fmt.label, h.style, fmt.reset) + end +end + +""" +$(TYPEDSIGNATURES) + +Compact one-line display of a [`VLine`](@ref). + +See also: `Base.show` +""" +function Base.show(io::IO, v::VLine) + fmt = Core.get_format_codes(io) + print(io, fmt.name, "VLine", fmt.reset, "(", fmt.value, v.value, fmt.reset, ")") +end + +""" +$(TYPEDSIGNATURES) + +Pretty display of a [`VLine`](@ref). + +See also: `Base.show` +""" +function Base.show(io::IO, ::MIME"text/plain", v::VLine) + fmt = Core.get_format_codes(io) + print(io, fmt.name, "VLine", fmt.reset, " at x=", fmt.value, v.value, fmt.reset) + if !isempty(v.style) + print(io, " style: ", fmt.label, v.style, fmt.reset) + end +end + +""" +$(TYPEDSIGNATURES) + +Compact one-line display of an [`Axes`](@ref). + +See also: `Base.show` +""" +function Base.show(io::IO, ax::Axes) + fmt = Core.get_format_codes(io) + print(io, fmt.name, "Axes", fmt.reset, "(") + print(io, fmt.label, repr(ax.title), fmt.reset, ", ") + print(io, fmt.value, length(ax.series), fmt.reset, " series)") +end + +""" +$(TYPEDSIGNATURES) + +Pretty tree-style display of an [`Axes`](@ref), recursively +showing its series and decorations. + +See also: `Base.show` +""" +function Base.show(io::IO, ::MIME"text/plain", ax::Axes) + _show_axes(io, ax, "") +end + +function _show_axes(io::IO, ax::Axes, prefix::String) + fmt = Core.get_format_codes(io) + n = length(ax.series) + print(io, fmt.name, "Axes", fmt.reset, " ") + print(io, fmt.label, repr(ax.title), fmt.reset) + print(io, " (", fmt.value, n, fmt.reset, " series)") + items = AbstractVector[] + for s in ax.series + push!(items, [s]) + end + for d in ax.decorations + push!(items, [d]) + end + if !isempty(ax.xlabel) || !isempty(ax.ylabel) + push!(items, [:labels, ax.xlabel, ax.ylabel]) + end + n_items = length(items) + for (i, item) in enumerate(items) + is_last = i == n_items + branch = is_last ? "└─ " : "├─ " + cont = is_last ? " " : "│ " + if length(item) == 1 && item[1] isa Series + s = item[1] + np = length(s.x) + print(io, "\n", prefix, branch) + print(io, fmt.name, "Series", fmt.reset, " ") + print(io, fmt.label, repr(s.label), fmt.reset) + print(io, " (", fmt.value, np, fmt.reset, np == 1 ? " point)" : " points)") + if !isempty(s.style) + print(io, "\n", prefix, cont, " style: ") + print(io, fmt.value, s.style, fmt.reset) + end + elseif length(item) == 1 + d = item[1] + print(io, "\n", prefix, branch, d) + elseif item[1] === :labels + print(io, "\n", prefix, branch) + if !isempty(ax.xlabel) + print(io, "xlabel: ", fmt.label, ax.xlabel, fmt.reset) + end + if !isempty(ax.xlabel) && !isempty(ax.ylabel) + print(io, ", ") + end + if !isempty(ax.ylabel) + print(io, "ylabel: ", fmt.label, ax.ylabel, fmt.reset) + end + end + end +end + +""" +$(TYPEDSIGNATURES) + +Compact one-line display of a [`Leaf`](@ref). + +See also: `Base.show` +""" +function Base.show(io::IO, leaf::Leaf) + fmt = Core.get_format_codes(io) + print(io, fmt.name, "Leaf", fmt.reset, "(") + show(io, leaf.axes) + print(io, ")") +end + +""" +$(TYPEDSIGNATURES) + +Pretty tree-style display of a [`Leaf`](@ref). + +See also: `Base.show` +""" +function Base.show(io::IO, ::MIME"text/plain", leaf::Leaf) + _show_node(io, leaf, "") +end + +""" +$(TYPEDSIGNATURES) + +Compact one-line display of an [`HBox`](@ref). + +See also: `Base.show` +""" +function Base.show(io::IO, box::HBox) + fmt = Core.get_format_codes(io) + print(io, fmt.name, "HBox", fmt.reset, "(") + print(io, fmt.value, length(box.children), fmt.reset, " children)") +end + +""" +$(TYPEDSIGNATURES) + +Pretty tree-style display of an [`HBox`](@ref), recursively +showing its children. + +See also: `Base.show` +""" +function Base.show(io::IO, ::MIME"text/plain", box::HBox) + _show_node(io, box, "") +end + +""" +$(TYPEDSIGNATURES) + +Compact one-line display of a [`VBox`](@ref). + +See also: `Base.show` +""" +function Base.show(io::IO, box::VBox) + fmt = Core.get_format_codes(io) + print(io, fmt.name, "VBox", fmt.reset, "(") + print(io, fmt.value, length(box.children), fmt.reset, " children)") +end + +""" +$(TYPEDSIGNATURES) + +Pretty tree-style display of a [`VBox`](@ref), recursively +showing its children. + +See also: `Base.show` +""" +function Base.show(io::IO, ::MIME"text/plain", box::VBox) + _show_node(io, box, "") +end + +# Recursively print a layout node. The node header is printed at the current +# cursor position; `prefix` is the indentation carried to the node's children. +function _show_node(io::IO, leaf::Leaf, prefix::String) + fmt = Core.get_format_codes(io) + print(io, fmt.name, "Leaf", fmt.reset) + print(io, "\n", prefix, "└─ ") + _show_axes(io, leaf.axes, prefix * " ") +end + +function _show_node(io::IO, node::Union{HBox,VBox}, prefix::String) + fmt = Core.get_format_codes(io) + type_name = node isa HBox ? "HBox" : "VBox" + n = length(node.children) + print(io, fmt.name, type_name, fmt.reset) + print(io, " (", fmt.value, n, fmt.reset, " children)") + shown = min(n, _SHOW_LIMIT) + for i in 1:shown + child = node.children[i] + is_last = i == n + branch = is_last ? "└─ " : "├─ " + childcont = is_last ? " " : "│ " + print(io, "\n", prefix, branch) + _show_node(io, child, prefix * childcont) + end + if n > _SHOW_LIMIT + print(io, "\n", prefix, fmt.muted, "… (", n - _SHOW_LIMIT, " more)", fmt.reset) + end +end + +""" +$(TYPEDSIGNATURES) + +Compact one-line display of a [`Figure`](@ref). + +See also: `Base.show` +""" +function Base.show(io::IO, fig::Figure) + fmt = Core.get_format_codes(io) + print(io, fmt.name, "Figure", fmt.reset, "(") + if fig.title !== nothing + print(io, fmt.label, repr(fig.title), fmt.reset, ", ") + end + show(io, fig.root) + print(io, ")") +end + +""" +$(TYPEDSIGNATURES) + +Pretty tree-style display of a [`Figure`](@ref), recursively +showing its layout tree. + +See also: `Base.show` +""" +function Base.show(io::IO, ::MIME"text/plain", fig::Figure) + fmt = Core.get_format_codes(io) + print(io, fmt.name, "Figure", fmt.reset) + if fig.title !== nothing + print(io, " ", fmt.label, repr(fig.title), fmt.reset) + end + if fig.size !== nothing + print(io, " size: ", fmt.value, fig.size, fmt.reset) + end + print(io, "\n└─ ") + _show_node(io, fig.root, " ") +end diff --git a/src/Plotting/panel.jl b/src/Plotting/panel.jl index f8479184..49fd594d 100644 --- a/src/Plotting/panel.jl +++ b/src/Plotting/panel.jl @@ -121,3 +121,44 @@ const _TITLE_FONT_SIZE = 10 Default axis-label font size in points, used by the Plots renderer. """ const _LABEL_FONT_SIZE = 10 + +# --- display ----------------------------------------------------------------- + +""" +$(TYPEDSIGNATURES) + +Compact one-line display of a [`Panel`](@ref). + +See also: `Base.show` +""" +function Base.show(io::IO, p::Panel) + fmt = Core.get_format_codes(io) + print(io, fmt.name, "Panel", fmt.reset, "(") + print(io, fmt.label, repr(p.title), fmt.reset, ", ") + print(io, fmt.value, n_components(p), fmt.reset, " components, ") + print(io, fmt.value, length(p.x), fmt.reset, " pts)") +end + +""" +$(TYPEDSIGNATURES) + +Pretty display of a [`Panel`](@ref), showing its title, labels, +and data dimensions. + +See also: `Base.show` +""" +function Base.show(io::IO, ::MIME"text/plain", p::Panel) + fmt = Core.get_format_codes(io) + n = n_components(p) + np = length(p.x) + print(io, fmt.name, "Panel", fmt.reset, " ") + print(io, fmt.label, repr(p.title), fmt.reset) + print(io, " (", fmt.value, n, fmt.reset, n == 1 ? " component, " : " components, ") + print(io, fmt.value, np, fmt.reset, np == 1 ? " point)" : " points)") + named = findall(!isempty, p.labels) + for (j, i) in enumerate(named) + is_last = j == length(named) + prefix = is_last ? "└─ " : "├─ " + print(io, "\n", prefix, fmt.label, p.labels[i], fmt.reset) + end +end diff --git a/test/suite/plotting/test_display.jl b/test/suite/plotting/test_display.jl new file mode 100644 index 00000000..6b10c5ef --- /dev/null +++ b/test/suite/plotting/test_display.jl @@ -0,0 +1,360 @@ +module TestPlottingDisplay + +using Test: Test +import CTBase.Plotting + +const VERBOSE = isdefined(Main, :TestData) ? Main.TestData.VERBOSE : true +const SHOWTIMING = isdefined(Main, :TestData) ? Main.TestData.SHOWTIMING : true + +# Helper: capture show output as a string (no color) +_sprint_show(x) = sprint(show, x) +_sprint_plain(x) = sprint(show, MIME"text/plain"(), x) + +# Helper builders +_series(label="x(t)", n=3) = Plotting.Series( + collect(Float64, 1:n), collect(Float64, 1:n); label=label, +) +_axes(; title="State", xlabel="t", ylabel="x", nseries=1) = Plotting.Axes( + [_series("s$i") for i in 1:nseries]; title=title, + xlabel=xlabel, ylabel=ylabel, +) +_leaf(; title="State") = Plotting.Leaf(_axes(; title=title)) + +function test_display() + Test.@testset "Plotting Display" verbose=VERBOSE showtiming=SHOWTIMING begin + + # ======================================================================== + # UNIT TESTS — compact show (one-line) + # ======================================================================== + + Test.@testset "Unit" begin + Test.@testset "Series compact" begin + s = _series("x(t)", 3) + out = _sprint_show(s) + Test.@test occursin("Series", out) + Test.@test occursin("x(t)", out) + Test.@test occursin("3", out) + Test.@test occursin("pts", out) + end + + Test.@testset "Series compact — single point" begin + s = _series("pt", 1) + out = _sprint_show(s) + Test.@test occursin("1", out) + Test.@test occursin("pts", out) + end + + Test.@testset "HLine compact" begin + h = Plotting.HLine(1.0) + out = _sprint_show(h) + Test.@test occursin("HLine", out) + Test.@test occursin("1.0", out) + end + + Test.@testset "VLine compact" begin + v = Plotting.VLine(2.5) + out = _sprint_show(v) + Test.@test occursin("VLine", out) + Test.@test occursin("2.5", out) + end + + Test.@testset "Axes compact" begin + ax = _axes(; title="State", nseries=2) + out = _sprint_show(ax) + Test.@test occursin("Axes", out) + Test.@test occursin("State", out) + Test.@test occursin("2", out) + Test.@test occursin("series", out) + end + + Test.@testset "Leaf compact" begin + leaf = _leaf() + out = _sprint_show(leaf) + Test.@test occursin("Leaf", out) + Test.@test occursin("Axes", out) + end + + Test.@testset "HBox compact" begin + box = Plotting.HBox([_leaf(), _leaf()]) + out = _sprint_show(box) + Test.@test occursin("HBox", out) + Test.@test occursin("2", out) + Test.@test occursin("children", out) + end + + Test.@testset "VBox compact" begin + box = Plotting.VBox([_leaf(), _leaf()]) + out = _sprint_show(box) + Test.@test occursin("VBox", out) + Test.@test occursin("2", out) + Test.@test occursin("children", out) + end + + Test.@testset "Figure compact" begin + fig = Plotting.Figure( + Plotting.VBox([_leaf()]); title="Solution", + ) + out = _sprint_show(fig) + Test.@test occursin("Figure", out) + Test.@test occursin("Solution", out) + Test.@test occursin("VBox", out) + end + + Test.@testset "Figure compact — no title" begin + fig = Plotting.Figure(Plotting.VBox([_leaf()])) + out = _sprint_show(fig) + Test.@test occursin("Figure", out) + Test.@test occursin("VBox", out) + end + + Test.@testset "Panel compact" begin + t = collect(0.0:0.1:1.0) + data = hcat(sin.(t), cos.(t)) + panel = Plotting.Panel(t, data; title="State") + out = _sprint_show(panel) + Test.@test occursin("Panel", out) + Test.@test occursin("State", out) + Test.@test occursin("2", out) + Test.@test occursin("components", out) + Test.@test occursin("11", out) + Test.@test occursin("pts", out) + end + end + + # ======================================================================== + # UNIT TESTS — pretty show (text/plain, tree-style) + # ======================================================================== + + Test.@testset "Unit — pretty" begin + Test.@testset "Series pretty" begin + s = Plotting.Series( + [0.0, 1.0, 2.0], [0.0, 1.0, 0.0]; + label="x(t)", style=(color=:blue, linewidth=2), + ) + out = _sprint_plain(s) + Test.@test occursin("Series", out) + Test.@test occursin("x(t)", out) + Test.@test occursin("3", out) + Test.@test occursin("points", out) + Test.@test occursin("style", out) + Test.@test occursin("color", out) + end + + Test.@testset "Series pretty — single point" begin + s = _series("pt", 1) + out = _sprint_plain(s) + Test.@test occursin("1", out) + Test.@test occursin("point)", out) + Test.@test !occursin("points)", out) + end + + Test.@testset "HLine pretty" begin + h = Plotting.HLine(1.0; style=(color=:red,)) + out = _sprint_plain(h) + Test.@test occursin("HLine", out) + Test.@test occursin("1.0", out) + Test.@test occursin("style", out) + end + + Test.@testset "VLine pretty" begin + v = Plotting.VLine(0.0) + out = _sprint_plain(v) + Test.@test occursin("VLine", out) + Test.@test occursin("0.0", out) + end + + Test.@testset "Axes pretty — series and labels" begin + ax = Plotting.Axes( + [_series("x(t)", 3), _series("y(t)", 2)]; + title="State", xlabel="t", ylabel="x", + ) + out = _sprint_plain(ax) + Test.@test occursin("Axes", out) + Test.@test occursin("State", out) + Test.@test occursin("2", out) + Test.@test occursin("series", out) + Test.@test occursin("x(t)", out) + Test.@test occursin("y(t)", out) + Test.@test occursin("xlabel", out) + Test.@test occursin("ylabel", out) + end + + Test.@testset "Axes pretty — decorations" begin + ax = Plotting.Axes( + [_series("s1", 1)]; + title="Titled", + decorations=[Plotting.HLine(1.0), Plotting.VLine(0.0)], + ) + out = _sprint_plain(ax) + Test.@test occursin("HLine", out) + Test.@test occursin("VLine", out) + end + + Test.@testset "Leaf pretty" begin + leaf = _leaf(; title="State") + out = _sprint_plain(leaf) + Test.@test occursin("Leaf", out) + Test.@test occursin("Axes", out) + Test.@test occursin("State", out) + Test.@test occursin("└─", out) + end + + Test.@testset "VBox pretty — tree structure" begin + tree = Plotting.VBox([_leaf(; title="A"), _leaf(; title="B")]) + out = _sprint_plain(tree) + Test.@test occursin("VBox", out) + Test.@test occursin("2", out) + Test.@test occursin("children", out) + Test.@test occursin("Leaf", out) + Test.@test occursin("A", out) + Test.@test occursin("B", out) + Test.@test occursin("├─", out) + Test.@test occursin("└─", out) + end + + Test.@testset "HBox pretty — tree structure" begin + box = Plotting.HBox([_leaf(; title="L"), _leaf(; title="R")]) + out = _sprint_plain(box) + Test.@test occursin("HBox", out) + Test.@test occursin("L", out) + Test.@test occursin("R", out) + end + + Test.@testset "Figure pretty — full tree" begin + leaf1 = _leaf(; title="State") + leaf2 = _leaf(; title="Control") + fig = Plotting.Figure( + Plotting.VBox([leaf1, leaf2]); title="Solution", + ) + out = _sprint_plain(fig) + Test.@test occursin("Figure", out) + Test.@test occursin("Solution", out) + Test.@test occursin("VBox", out) + Test.@test occursin("State", out) + Test.@test occursin("Control", out) + end + + Test.@testset "Figure pretty — size shown" begin + fig = Plotting.Figure( + _leaf(); title="Fig", size=(800, 600), + ) + out = _sprint_plain(fig) + Test.@test occursin("size", out) + Test.@test occursin("800", out) + end + + Test.@testset "Panel pretty — labels shown" begin + t = collect(0.0:0.1:1.0) + data = hcat(sin.(t), cos.(t)) + panel = Plotting.Panel(t, data; title="State", labels=["x1", "x2"]) + out = _sprint_plain(panel) + Test.@test occursin("Panel", out) + Test.@test occursin("State", out) + Test.@test occursin("2", out) + Test.@test occursin("components", out) + Test.@test occursin("11", out) + Test.@test occursin("points", out) + Test.@test occursin("x1", out) + Test.@test occursin("x2", out) + end + + Test.@testset "Panel pretty — no empty labels" begin + t = collect(0.0:0.1:1.0) + data = hcat(sin.(t), cos.(t)) + panel = Plotting.Panel(t, data; title="State") + out = _sprint_plain(panel) + lines = split(out, '\n') + Test.@test length(lines) == 1 + end + + Test.@testset "Panel pretty — partial labels" begin + t = collect(0.0:0.1:1.0) + data = hcat(sin.(t), cos.(t), t) + panel = Plotting.Panel( + t, data; title="Mix", labels=["a", "", "c"], + ) + out = _sprint_plain(panel) + Test.@test occursin("a", out) + Test.@test occursin("c", out) + lines = split(out, '\n') + Test.@test length(lines) == 3 + end + + Test.@testset "Panel pretty — singular" begin + panel = Plotting.Panel( + [0.0], reshape([1.0], :, 1); + title="S", labels=["u"], + ) + out = _sprint_plain(panel) + Test.@test occursin("1 component", out) + Test.@test occursin("1 point", out) + Test.@test !occursin("components", out) + Test.@test !occursin("points", out) + end + end + + # ======================================================================== + # INTEGRATION TESTS — nested trees and truncation + # ======================================================================== + + Test.@testset "Integration" begin + Test.@testset "Nested HBox in VBox" begin + inner = Plotting.HBox([_leaf(; title="L"), _leaf(; title="R")]) + outer = Plotting.VBox([inner, _leaf(; title="B")]) + out = _sprint_plain(outer) + Test.@test occursin("VBox", out) + Test.@test occursin("HBox", out) + Test.@test occursin("L", out) + Test.@test occursin("R", out) + Test.@test occursin("B", out) + end + + Test.@testset "Truncation beyond limit" begin + leaves = [ + _leaf(; title="C$i") for i in 1:8 + ] + box = Plotting.VBox(leaves) + out = _sprint_plain(box) + Test.@test occursin("8", out) + Test.@test occursin("more", out) + Test.@test occursin("C1", out) + Test.@test occursin("C5", out) + Test.@test !occursin("C6", out) + end + + Test.@testset "Figure with nested tree" begin + leaf1 = _leaf(; title="State") + leaf2 = _leaf(; title="Costate") + leaf3 = _leaf(; title="Control") + tree = Plotting.VBox([ + Plotting.HBox([leaf1, leaf2]), + leaf3, + ]) + fig = Plotting.Figure(tree; title="Solution") + out = _sprint_plain(fig) + Test.@test occursin("Figure", out) + Test.@test occursin("Solution", out) + Test.@test occursin("VBox", out) + Test.@test occursin("HBox", out) + Test.@test occursin("State", out) + Test.@test occursin("Costate", out) + Test.@test occursin("Control", out) + end + + Test.@testset "Axes with many series — no truncation" begin + ss = [_series("s$i") for i in 1:7] + ax = Plotting.Axes(ss; title="Many") + out = _sprint_plain(ax) + for i in 1:7 + Test.@test occursin("s$i", out) + end + Test.@test occursin("7", out) + end + end + end + return nothing +end + +end # module TestPlottingDisplay + +test_display() = TestPlottingDisplay.test_display() From e8c2889f9d9fc15ff82560db08bb2e6549b50143 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Sat, 11 Jul 2026 16:13:36 +0200 Subject: [PATCH 3/3] docs: add setup block to plotting guide to suppress image output in examples Add @setup plot block that loads Plots and disables image/png showable to prevent inline rendering in docs. Update render example text to note that the ExtensionError cannot be demonstrated because Plots is loaded by make.jl. Move the render(fig) example after the explanatory text. --- docs/src/guide/plotting.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/docs/src/guide/plotting.md b/docs/src/guide/plotting.md index 471e1d6e..9dadad0b 100644 --- a/docs/src/guide/plotting.md +++ b/docs/src/guide/plotting.md @@ -4,6 +4,11 @@ CurrentModule = CTBase ``` +```@setup plot +using Plots +Base.showable(::MIME"image/png", ::Plots.Plot) = false +``` + The [`CTBase.Plotting`](@ref) submodule is a generic, domain-free plotting engine. It manipulates a backend-agnostic **intermediate representation** (IR): a weighted tree of titled axes carrying series and decorations. The engine knows nothing about @@ -178,19 +183,16 @@ a `Figure` onto an existing backend target, targeting cells by the deterministic leaf order (see [`leaves`](@ref)). Without a backend loaded, the fallback throws an -[`CTBase.Exceptions.ExtensionError`](@ref): +[`CTBase.Exceptions.ExtensionError`](@ref). This cannot be demonstrated +in these docs because `Plots` is loaded by `make.jl` to produce the +examples below, which causes the `CTBasePlots` extension to be active. + +Once `Plots` is loaded, `render(fig)` produces a Plots.jl plot: ```@example plot -fig2 = CTBase.Plotting.Figure(CTBase.Plotting.Leaf(ax)) -try # hide -CTBase.Plotting.render(fig2) -catch e # hide -showerror(IOContext(stdout, :color => false), e) # hide -end # hide +CTBase.Plotting.render(fig) ``` -Once `Plots` is loaded, `render(fig)` produces a Plots.jl plot. - ## Leaf Traversal [`leaves`](@ref) returns the `Leaf` nodes of a layout tree in deterministic