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/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 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 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()