From 97b24549568e3eb95ee35cc4a2c7334b572b8b45 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Thu, 9 Jul 2026 21:52:12 +0200 Subject: [PATCH] feat: constraint-kind trait family and path-constraint/multiplier carriers - Add AbstractConstraintKind trait family (Traits/constraint_kind.jl): StateConstraintKind, ControlConstraintKind, MixedConstraintKind - Add constraint_kind() accessor and predicates (is_state/control/mixed_constraint) - Wire into Traits.jl (includes, exports, module docstring) - Add AbstractPathConstraint{K,TD,VD} abstract type (Data/abstract_path_constraint.jl) - Add PathConstraint{F,K,TD,VD} concrete type with natural/uniform call signatures - User-facing constructors: StateConstraint(), ControlConstraint(), MixedConstraint() - Add AbstractMultiplier{TD,VD} abstract type (Data/abstract_multiplier.jl) - Add Multiplier{F,TD,VD} concrete type with natural/uniform call signatures - Add display helpers in Data/helpers.jl (_kind_label, _natural_sig_pc, _uniform_sig_pc, _natural_sig_mult, _uniform_sig_mult) - Wire into Data.jl (includes, exports) - Update test_data_module.jl export/private symbol lists - Add 5 new test files: test_constraint_kind.jl, test_abstract_path_constraint.jl, test_path_constraint.jl, test_abstract_multiplier.jl, test_multiplier.jl - Full CTBase test suite: 4579/4579 passing (Aqua included) Co-Authored-By: Claude Sonnet 5 --- CHANGELOGS.md | 30 ++ Project.toml | 2 +- src/Data/Data.jl | 11 + src/Data/abstract_multiplier.jl | 92 ++++ src/Data/abstract_path_constraint.jl | 165 +++++++ src/Data/helpers.jl | 133 +++++ src/Data/multiplier.jl | 189 +++++++ src/Data/path_constraint.jl | 460 ++++++++++++++++++ src/Traits/Traits.jl | 8 +- src/Traits/constraint_kind.jl | 117 +++++ test/suite/data/test_abstract_multiplier.jl | 80 +++ .../data/test_abstract_path_constraint.jl | 123 +++++ test/suite/data/test_data_module.jl | 13 + test/suite/data/test_multiplier.jl | 109 +++++ test/suite/data/test_path_constraint.jl | 183 +++++++ test/suite/traits/test_constraint_kind.jl | 89 ++++ 16 files changed, 1802 insertions(+), 2 deletions(-) create mode 100644 src/Data/abstract_multiplier.jl create mode 100644 src/Data/abstract_path_constraint.jl create mode 100644 src/Data/multiplier.jl create mode 100644 src/Data/path_constraint.jl create mode 100644 src/Traits/constraint_kind.jl create mode 100644 test/suite/data/test_abstract_multiplier.jl create mode 100644 test/suite/data/test_abstract_path_constraint.jl create mode 100644 test/suite/data/test_multiplier.jl create mode 100644 test/suite/data/test_path_constraint.jl create mode 100644 test/suite/traits/test_constraint_kind.jl diff --git a/CHANGELOGS.md b/CHANGELOGS.md index 7bdbba3b..04fc6c07 100644 --- a/CHANGELOGS.md +++ b/CHANGELOGS.md @@ -5,6 +5,36 @@ All notable changes to CTBase will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.27.3-beta] - 2026-07-09 + +### ✨ New Features + +#### **Traits** — constraint-kind trait family + +- **New `AbstractConstraintKind` trait family**: distinguishes path constraints by which + primal variables they depend on. Three concrete traits: `StateConstraintKind` (state only), + `ControlConstraintKind` (control only), `MixedConstraintKind` (state and control). + Type-parameter-only contract (like `AbstractFeedback`). Added `constraint_kind()` accessor + and predicates `is_state_constraint()`, `is_control_constraint()`, `is_mixed_constraint()`. + +#### **Data** — path constraint and multiplier carriers + +- **New `AbstractPathConstraint{K,TD,VD}` and `PathConstraint{F,K,TD,VD}` types**: generic + carriers for path-constraint functions with constraint-kind, time-dependence, and + variable-dependence traits. Supports natural call signatures per kind (e.g. `g(x)` for + state, `g(u)` for control, `g(x,u)` for mixed) and uniform signature `g(t,x,u,v)`. + User-facing constructors: `StateConstraint(g)`, `ControlConstraint(g)`, `MixedConstraint(g)`. +- **New `AbstractMultiplier{TD,VD}` and `Multiplier{F,TD,VD}` types**: carriers for + Lagrange multiplier functions with time and variable dependence. Call signature `μ(t,x,p,v)` + (uniform) or natural forms `μ(x,p)`, `μ(t,x,p)`, etc. Semantically parallel to `Hamiltonian`. +- **Display helpers**: `_kind_label`, `_natural_sig_pc` / `_natural_args_pc` / `_uniform_sig_pc`, + `_natural_sig_mult` / `_uniform_sig_mult` for `show` support. + +### 🔧 Compatibility + +- No breaking changes. New types and traits expand the API surface for path constraints + without affecting existing code. + ## [0.27.2-beta] - 2026-07-09 ### 🐛 Fixed diff --git a/Project.toml b/Project.toml index 0c4f06df..6d0322bb 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "CTBase" uuid = "54762871-cc72-4466-b8e8-f6c8b58076cd" -version = "0.27.2-beta" +version = "0.27.3-beta" authors = ["Olivier Cots ", "Jean-Baptiste Caillau "] [deps] diff --git a/src/Data/Data.jl b/src/Data/Data.jl index 9444302e..d792b5cd 100644 --- a/src/Data/Data.jl +++ b/src/Data/Data.jl @@ -25,6 +25,10 @@ include(joinpath(@__DIR__, "abstract_hamiltonian.jl")) include(joinpath(@__DIR__, "hamiltonian.jl")) include(joinpath(@__DIR__, "abstract_control_law.jl")) include(joinpath(@__DIR__, "control_law.jl")) +include(joinpath(@__DIR__, "abstract_path_constraint.jl")) +include(joinpath(@__DIR__, "path_constraint.jl")) +include(joinpath(@__DIR__, "abstract_multiplier.jl")) +include(joinpath(@__DIR__, "multiplier.jl")) include(joinpath(@__DIR__, "abstract_pseudo_hamiltonian.jl")) include(joinpath(@__DIR__, "pseudo_hamiltonian.jl")) include(joinpath(@__DIR__, "composed_hamiltonian.jl")) @@ -49,6 +53,13 @@ export ControlLaw export OpenLoop export ClosedLoop export DynClosedLoop +export AbstractPathConstraint +export PathConstraint +export StateConstraint +export ControlConstraint +export MixedConstraint +export AbstractMultiplier +export Multiplier export AbstractPseudoHamiltonian export PseudoHamiltonian export ComposedHamiltonian diff --git a/src/Data/abstract_multiplier.jl b/src/Data/abstract_multiplier.jl new file mode 100644 index 00000000..cdc4da0a --- /dev/null +++ b/src/Data/abstract_multiplier.jl @@ -0,0 +1,92 @@ +""" +$(TYPEDEF) + +Abstract supertype for path-constraint multiplier functions together with their +time-dependence and variable-dependence traits. + +A multiplier is a function `μ(t, x, p[, v])` returning the Lagrange multiplier +associated with a path constraint. It has the same call structure as a +[`CTBase.Data.AbstractHamiltonian`](@ref) (it depends on the state and costate), but +carries no dynamics semantics of its own. + +# Type Parameters +- `TD <: TimeDependence`: `Autonomous` or `NonAutonomous`. +- `VD <: VariableDependence`: `Fixed` or `NonFixed`. + +# Notes +- All multiplier types support both natural and uniform call signatures. +- The uniform signature `(t, x, p, v)` is used internally by flows. + +See also: [`CTBase.Data.Multiplier`](@ref), [`CTBase.Traits.TimeDependence`](@ref), [`CTBase.Traits.VariableDependence`](@ref). +""" +abstract type AbstractMultiplier{TD<:Traits.TimeDependence,VD<:Traits.VariableDependence} end + +# ============================================================================= +# Trait accessors for AbstractMultiplier +# ============================================================================= + +""" +$(TYPEDSIGNATURES) + +Indicates that all `AbstractMultiplier` types support time-dependence queries. + +# Returns +- `true`: Always returns `true` for multiplier types. + +See also: [`CTBase.Traits.time_dependence`](@ref), [`CTBase.Data.AbstractMultiplier`](@ref). +""" +function Traits.has_time_dependence_trait(::AbstractMultiplier) + return true +end + +""" +$(TYPEDSIGNATURES) + +Indicates that all `AbstractMultiplier` types support variable-dependence queries. + +# Returns +- `true`: Always returns `true` for multiplier types. + +See also: [`CTBase.Traits.variable_dependence`](@ref), [`CTBase.Data.AbstractMultiplier`](@ref). +""" +function Traits.has_variable_dependence_trait(::AbstractMultiplier) + return true +end + +""" +$(TYPEDSIGNATURES) + +Return the time-dependence trait of a multiplier. + +# Arguments +- `m::AbstractMultiplier`: The multiplier object. + +# Returns +- `TD`: The time-dependence type (`Autonomous` or `NonAutonomous`). + +See also: [`CTBase.Traits.time_dependence`](@ref), [`CTBase.Traits.TimeDependence`](@ref). +""" +function Traits.time_dependence( + ::AbstractMultiplier{TD,<:Traits.VariableDependence} +) where {TD<:Traits.TimeDependence} + return TD +end + +""" +$(TYPEDSIGNATURES) + +Return the variable-dependence trait of a multiplier. + +# Arguments +- `m::AbstractMultiplier`: The multiplier object. + +# Returns +- `VD`: The variable-dependence type (`Fixed` or `NonFixed`). + +See also: [`CTBase.Traits.variable_dependence`](@ref), [`CTBase.Traits.VariableDependence`](@ref). +""" +function Traits.variable_dependence( + ::AbstractMultiplier{<:Traits.TimeDependence,VD} +) where {VD<:Traits.VariableDependence} + return VD +end diff --git a/src/Data/abstract_path_constraint.jl b/src/Data/abstract_path_constraint.jl new file mode 100644 index 00000000..1111d637 --- /dev/null +++ b/src/Data/abstract_path_constraint.jl @@ -0,0 +1,165 @@ +# ============================================================================= +# AbstractPathConstraint — abstract supertype for path constraints +# ============================================================================= + +""" +$(TYPEDEF) + +Abstract supertype for path constraints together with their constraint-kind, +time-dependence, and variable-dependence traits. + +A path constraint is a function `g(...)` evaluated along the trajectory of an +optimal control problem. The constraint-kind trait +([`CTBase.Traits.AbstractConstraintKind`](@ref)) determines which primal variables the +constraint depends on: + +- **State**: `g(x)` — depends on the state (and optionally time and variable). +- **Control**: `g(u)` — depends on the control (and optionally time and variable). +- **Mixed**: `g(x, u)` — depends on both state and control. + +# Type Parameters +- `K <: AbstractConstraintKind`: `StateConstraintKind`, `ControlConstraintKind`, or `MixedConstraintKind`. +- `TD <: TimeDependence`: `Autonomous` or `NonAutonomous`. +- `VD <: VariableDependence`: `Fixed` or `NonFixed`. + +# Notes +- All path constraint types support both natural and uniform call signatures. +- The uniform signature is always `g(t, x, u, v)`, ignoring the unused arguments. + +See also: [`CTBase.Data.PathConstraint`](@ref), [`CTBase.Traits.AbstractConstraintKind`](@ref), +[`CTBase.Traits.TimeDependence`](@ref), [`CTBase.Traits.VariableDependence`](@ref). +""" +abstract type AbstractPathConstraint{ + K<:Traits.AbstractConstraintKind, + TD<:Traits.TimeDependence, + VD<:Traits.VariableDependence, +} end + +# ============================================================================= +# Trait accessors for AbstractPathConstraint +# ============================================================================= + +""" +$(TYPEDSIGNATURES) + +Return the constraint-kind trait of a path constraint. + +# Returns +- `K`: The constraint-kind type (`StateConstraintKind`, `ControlConstraintKind`, or `MixedConstraintKind`). + +See also: [`CTBase.Traits.constraint_kind`](@ref), [`CTBase.Traits.AbstractConstraintKind`](@ref). +""" +function Traits.constraint_kind( + ::AbstractPathConstraint{K,<:Any,<:Any} +) where {K<:Traits.AbstractConstraintKind} + return K +end + +""" +$(TYPEDSIGNATURES) + +Indicates that all `AbstractPathConstraint` types support time-dependence queries. + +# Returns +- `true`: Always returns `true` for path constraint types. + +See also: [`CTBase.Traits.time_dependence`](@ref), [`CTBase.Data.AbstractPathConstraint`](@ref). +""" +function Traits.has_time_dependence_trait(::AbstractPathConstraint) + return true +end + +""" +$(TYPEDSIGNATURES) + +Indicates that all `AbstractPathConstraint` types support variable-dependence queries. + +# Returns +- `true`: Always returns `true` for path constraint types. + +See also: [`CTBase.Traits.variable_dependence`](@ref), [`CTBase.Data.AbstractPathConstraint`](@ref). +""" +function Traits.has_variable_dependence_trait(::AbstractPathConstraint) + return true +end + +""" +$(TYPEDSIGNATURES) + +Return the time-dependence trait of a path constraint. + +# Arguments +- `pc::AbstractPathConstraint`: The path constraint object. + +# Returns +- `TD`: The time-dependence type (`Autonomous` or `NonAutonomous`). + +See also: [`CTBase.Traits.time_dependence`](@ref), [`CTBase.Traits.TimeDependence`](@ref). +""" +function Traits.time_dependence( + ::AbstractPathConstraint{<:Any,TD,<:Traits.VariableDependence} +) where {TD<:Traits.TimeDependence} + return TD +end + +""" +$(TYPEDSIGNATURES) + +Return the variable-dependence trait of a path constraint. + +# Arguments +- `pc::AbstractPathConstraint`: The path constraint object. + +# Returns +- `VD`: The variable-dependence type (`Fixed` or `NonFixed`). + +See also: [`CTBase.Traits.variable_dependence`](@ref), [`CTBase.Traits.VariableDependence`](@ref). +""" +function Traits.variable_dependence( + ::AbstractPathConstraint{<:Any,<:Traits.TimeDependence,VD} +) where {VD<:Traits.VariableDependence} + return VD +end + +# ============================================================================= +# Constraint-kind predicates — dispatch on the K type parameter +# ============================================================================= + +""" +$(TYPEDSIGNATURES) + +Return `true` if the path constraint is a pure state constraint, `false` otherwise. + +# Returns +- `Bool`: `true` if the constraint-kind trait is `StateConstraintKind`, `false` otherwise. + +See also: [`CTBase.Traits.StateConstraintKind`](@ref), [`CTBase.Traits.constraint_kind`](@ref). +""" +Traits.is_state_constraint(::AbstractPathConstraint) = false +Traits.is_state_constraint(::AbstractPathConstraint{<:Traits.StateConstraintKind}) = true + +""" +$(TYPEDSIGNATURES) + +Return `true` if the path constraint is a pure control constraint, `false` otherwise. + +# Returns +- `Bool`: `true` if the constraint-kind trait is `ControlConstraintKind`, `false` otherwise. + +See also: [`CTBase.Traits.ControlConstraintKind`](@ref), [`CTBase.Traits.constraint_kind`](@ref). +""" +Traits.is_control_constraint(::AbstractPathConstraint) = false +Traits.is_control_constraint(::AbstractPathConstraint{<:Traits.ControlConstraintKind}) = true + +""" +$(TYPEDSIGNATURES) + +Return `true` if the path constraint is a mixed state–control constraint, `false` otherwise. + +# Returns +- `Bool`: `true` if the constraint-kind trait is `MixedConstraintKind`, `false` otherwise. + +See also: [`CTBase.Traits.MixedConstraintKind`](@ref), [`CTBase.Traits.constraint_kind`](@ref). +""" +Traits.is_mixed_constraint(::AbstractPathConstraint) = false +Traits.is_mixed_constraint(::AbstractPathConstraint{<:Traits.MixedConstraintKind}) = true diff --git a/src/Data/helpers.jl b/src/Data/helpers.jl index fb750f4d..f499bb55 100644 --- a/src/Data/helpers.jl +++ b/src/Data/helpers.jl @@ -394,3 +394,136 @@ See also: [`CTBase.Data._natural_sig_ph`](@ref). function _uniform_sig_ph() return "h̃(t, x, p, u, v)" end + +# ============================================================================= +# Constraint-kind label helpers +# ============================================================================= + +""" + _kind_label(::Type{Traits.StateConstraintKind}) -> String + _kind_label(::Type{Traits.ControlConstraintKind}) -> String + _kind_label(::Type{Traits.MixedConstraintKind}) -> String + +Return a user-friendly label for constraint-kind traits. + +# Returns +- `String`: "state", "control", or "mixed". + +See also: [`CTBase.Data._td_label`](@ref), [`CTBase.Data._vd_label`](@ref). +""" +function _kind_label(::Type{Traits.StateConstraintKind}) + return "state" +end +function _kind_label(::Type{Traits.ControlConstraintKind}) + return "control" +end +function _kind_label(::Type{Traits.MixedConstraintKind}) + return "mixed" +end + +# ============================================================================= +# PathConstraint-specific signature helpers +# ============================================================================= + +""" + _natural_sig_pc(::Type{K}, ::Type{TD}, ::Type{VD}) where {K, TD, VD} -> String + +Return the natural call signature for a `PathConstraint` based on its traits. + +The arguments depend on the constraint kind: +- `StateConstraintKind`: `g([t, ]x[, v])` +- `ControlConstraintKind`: `g([t, ]u[, v])` +- `MixedConstraintKind`: `g([t, ]x, u[, v])` + +See also: [`CTBase.Data._uniform_sig_pc`](@ref). +""" +function _natural_sig_pc( + ::Type{K}, ::Type{TD}, ::Type{VD} +) where { + K<:Traits.AbstractConstraintKind, + TD<:Traits.TimeDependence, + VD<:Traits.VariableDependence, +} + args = String[] + TD === Traits.NonAutonomous && push!(args, "t") + append!(args, _natural_args_pc(K)) + VD === Traits.NonFixed && push!(args, "v") + return "g(" * join(args, ", ") * ")" +end + +""" + _natural_args_pc(::Type{K}) where {K} -> Vector{String} + +Return the kind-dependent argument names (excluding `t` and `v`) for the natural +call signature of a `PathConstraint`. + +- `StateConstraintKind`: `["x"]`. +- `ControlConstraintKind`: `["u"]`. +- `MixedConstraintKind`: `["x", "u"]`. + +See also: [`CTBase.Data._natural_sig_pc`](@ref). +""" +function _natural_args_pc(::Type{Traits.StateConstraintKind}) + return ["x"] +end + +function _natural_args_pc(::Type{Traits.ControlConstraintKind}) + return ["u"] +end + +function _natural_args_pc(::Type{Traits.MixedConstraintKind}) + return ["x", "u"] +end + +""" + _uniform_sig_pc() -> String + +Return the uniform call signature for a `PathConstraint`. + +The uniform signature always includes all arguments `(t, x, u, v)` regardless of traits. + +See also: [`CTBase.Data._natural_sig_pc`](@ref). +""" +function _uniform_sig_pc() + return "g(t, x, u, v)" +end + +# ============================================================================= +# Multiplier-specific signature helpers +# ============================================================================= + +""" + _natural_sig_mult(::Type{TD}, ::Type{VD}) where {TD, VD} -> String + +Return the natural call signature for a `Multiplier` based on its traits. + +# Arguments +- `TD`: Time dependence type (`Autonomous` or `NonAutonomous`) +- `VD`: Variable dependence type (`Fixed` or `NonFixed`) + +# Returns +- `String`: Natural call signature (e.g., "μ(x, p)", "μ(t, x, p)") + +See also: [`CTBase.Data._uniform_sig_mult`](@ref). +""" +function _natural_sig_mult(::Type{TD}, ::Type{VD}) where {TD,VD} + args = String[] + TD === Traits.NonAutonomous && push!(args, "t") + push!(args, "x") + push!(args, "p") + VD === Traits.NonFixed && push!(args, "v") + return "μ(" * join(args, ", ") * ")" +end + +""" + _uniform_sig_mult() -> String + +Return the uniform call signature for a `Multiplier`. + +The uniform signature always includes all arguments `(t, x, p, v)` regardless of traits. + +See also: [`CTBase.Data._natural_sig_mult`](@ref). +""" +function _uniform_sig_mult() + return "μ(t, x, p, v)" +end diff --git a/src/Data/multiplier.jl b/src/Data/multiplier.jl new file mode 100644 index 00000000..465fda6b --- /dev/null +++ b/src/Data/multiplier.jl @@ -0,0 +1,189 @@ +# ============================================================================= +# Concrete Multiplier type +# ============================================================================= + +""" +$(TYPEDEF) + +Parametric container for a path-constraint multiplier function together with its +time-dependence and variable-dependence traits. + +The function returns the Lagrange multiplier `μ(t, x, p[, v])` associated with a +path constraint. It has the same call structure as a +[`CTBase.Data.Hamiltonian`](@ref). + +# Type Parameters +- `F`: concrete type of the wrapped function. +- `TD <: TimeDependence`: `Autonomous` or `NonAutonomous`. +- `VD <: VariableDependence`: `Fixed` or `NonFixed`. + +# Fields +- `f::F`: the multiplier function. + +# Construction + +Use the keyword constructor: + +```julia +Multiplier(f; is_autonomous = true, is_variable = false) # default: μ(x, p) +Multiplier((t, x, p) -> ...; is_autonomous = false) # μ(t, x, p) +Multiplier((x, p, v) -> ...; is_variable = true) # μ(x, p, v) +Multiplier((t, x, p, v) -> ...; is_autonomous = false, is_variable = true) +``` + +# Call Signatures + +Every `Multiplier` is callable via its **natural** signature (matching the +traits), and via a **uniform** signature `(t, x, p, v)` that ignores the unused +arguments. + +For Autonomous/Fixed: natural `μ(x, p)`, uniform `μ(t, x, p, v)`. +For NonAutonomous/Fixed: natural `μ(t, x, p)`, uniform `μ(t, x, p, v)`. +For Autonomous/NonFixed: natural `μ(x, p, v)`, uniform `μ(t, x, p, v)`. +For NonAutonomous/NonFixed: natural `μ(t, x, p, v)`, uniform `μ(t, x, p, v)`. + +# Example +```julia-repl +julia> using CTBase.Data + +julia> μ = Multiplier((x, p) -> x[1]) +Multiplier: autonomous, fixed (no variable) + natural call: μ(x, p) + uniform call: μ(t, x, p, v) +``` + +See also: [`CTBase.Data.AbstractMultiplier`](@ref), [`CTBase.Data.Hamiltonian`](@ref), +[`CTBase.Traits.TimeDependence`](@ref), [`CTBase.Traits.VariableDependence`](@ref). +""" +struct Multiplier{F<:Function,TD,VD} <: AbstractMultiplier{TD,VD} + f::F +end + +# ============================================================================= +# Constructor +# ============================================================================= + +""" +$(TYPEDSIGNATURES) + +Construct a `Multiplier` with trait flags. + +# Arguments +- `f::Function`: The multiplier function. +- `is_autonomous::Bool`: If true, multiplier is autonomous (default: `__is_autonomous()`). +- `is_variable::Bool`: If true, multiplier depends on variable parameters (default: `__is_variable()`). + +# Returns +- `Multiplier`: A multiplier with appropriate traits. + +# Example +```julia-repl +julia> using CTBase.Data + +julia> μ = Multiplier((x, p) -> x[1]) +Multiplier: autonomous, fixed (no variable) + natural call: μ(x, p) + uniform call: μ(t, x, p, v) + +julia> μ = Multiplier((t, x, p) -> t * x[1]; is_autonomous=false) +Multiplier: non-autonomous, fixed (no variable) + natural call: μ(t, x, p) + uniform call: μ(t, x, p, v) +``` + +See also: [`CTBase.Data.Multiplier`](@ref), [`CTBase.Traits.Autonomous`](@ref), +[`CTBase.Traits.NonAutonomous`](@ref), [`CTBase.Traits.Fixed`](@ref), [`CTBase.Traits.NonFixed`](@ref). +""" +function Multiplier( + f; is_autonomous::Bool=__is_autonomous(), is_variable::Bool=__is_variable() +) + TD = is_autonomous ? Traits.Autonomous : Traits.NonAutonomous + VD = is_variable ? Traits.NonFixed : Traits.Fixed + return Multiplier{typeof(f),TD,VD}(f) +end + +# ============================================================================= +# Typed constructor — calls struct inner constructor directly +# ============================================================================= + +""" +$(TYPEDSIGNATURES) + +Typed constructor for `Multiplier` with explicit trait types. + +See also: [`CTBase.Data.Multiplier`](@ref). +""" +function Multiplier( + f, ::Type{TD}, ::Type{VD} +) where {TD<:Traits.TimeDependence,VD<:Traits.VariableDependence} + return Multiplier{typeof(f),TD,VD}(f) +end + +# ============================================================================= +# Natural call signatures - one per trait combination +# ============================================================================= + +(m::Multiplier{<:Function,Traits.Autonomous,Traits.Fixed})(x, p) = m.f(x, p) +(m::Multiplier{<:Function,Traits.NonAutonomous,Traits.Fixed})(t, x, p) = m.f(t, x, p) +(m::Multiplier{<:Function,Traits.Autonomous,Traits.NonFixed})(x, p, v) = m.f(x, p, v) +function (m::Multiplier{<:Function,Traits.NonAutonomous,Traits.NonFixed})(t, x, p, v) + return m.f(t, x, p, v) +end + +# ============================================================================= +# Uniform (t, x, p, v) call — used by flows. +# Every combination forwards to its natural call, ignoring unused args. +# (NonAutonomous, NonFixed) is already covered by the natural signature above. +# ============================================================================= + +(m::Multiplier{<:Function,Traits.Autonomous,Traits.Fixed})(_, x, p, _) = m.f(x, p) +(m::Multiplier{<:Function,Traits.NonAutonomous,Traits.Fixed})(t, x, p, _) = m.f(t, x, p) +(m::Multiplier{<:Function,Traits.Autonomous,Traits.NonFixed})(_, x, p, v) = m.f(x, p, v) + +# ============================================================================= +# Base.show +# ============================================================================= + +""" +$(TYPEDSIGNATURES) + +Display a compact representation of a `Multiplier` showing its traits and call signatures. + +# Arguments +- `io::IO`: The IO stream. +- `m::Multiplier`: The multiplier object. + +# Output +Displays three lines: +- Header with time and variable dependence traits +- Natural call signature +- Uniform call signature + +See also: [`CTBase.Data.Multiplier`](@ref). +""" +function Base.show(io::IO, ::Multiplier{F,TD,VD}) where {F,TD,VD} + header = "Multiplier: $(_td_label(TD)), $(_vd_label(VD))" + natural = _natural_sig_mult(TD, VD) + uniform = _uniform_sig_mult() + println(io, header) + println(io, " natural call: ", natural) + return print(io, " uniform call: ", uniform) +end + +""" +$(TYPEDSIGNATURES) + +Display a `Multiplier` in the REPL with the same format as the compact `show`. + +This method is called automatically when displaying a multiplier in the Julia REPL. + +# Arguments +- `io::IO`: The IO stream. +- `mime::MIME"text/plain"`: The MIME type. +- `m::Multiplier`: The multiplier object. + +See also: [`CTBase.Data.Multiplier`](@ref). +""" +function Base.show(io::IO, ::MIME"text/plain", m::Multiplier{F,TD,VD}) where {F,TD,VD} + return show(io, m) +end diff --git a/src/Data/path_constraint.jl b/src/Data/path_constraint.jl new file mode 100644 index 00000000..a342ec12 --- /dev/null +++ b/src/Data/path_constraint.jl @@ -0,0 +1,460 @@ +# ============================================================================= +# Concrete PathConstraint type +# ============================================================================= + +""" +$(TYPEDEF) + +Parametric container for a path-constraint function together with its +constraint-kind, time-dependence, and variable-dependence traits. + +The function returns the value `g(...)` of a path constraint evaluated along the +trajectory of an optimal control problem. The constraint-kind trait determines +which primal variables the constraint depends on (see +[`CTBase.Data.AbstractPathConstraint`](@ref)). + +# Type Parameters +- `F`: concrete type of the wrapped function. +- `K <: AbstractConstraintKind`: `StateConstraintKind`, `ControlConstraintKind`, or `MixedConstraintKind`. +- `TD <: TimeDependence`: `Autonomous` or `NonAutonomous`. +- `VD <: VariableDependence`: `Fixed` or `NonFixed`. + +# Fields +- `f::F`: the path-constraint function. + +# Construction + +Use the user-facing constructors `StateConstraint`, `ControlConstraint`, or `MixedConstraint`: + +```julia +StateConstraint(g; is_autonomous = true, is_variable = false) # default: g(x) +ControlConstraint(g; is_autonomous = true, is_variable = false) # default: g(u) +MixedConstraint(g; is_autonomous = true, is_variable = false) # default: g(x, u) +``` + +# Call Signatures + +Every `PathConstraint` is callable via its **natural** signature (matching the +traits), and via the **uniform** signature `g(t, x, u, v)` (ignoring unused +arguments), used by flow integrators: + +| Kind | Natural `(Aut, Fixed)` | Uniform | +|---|---|---| +| `StateConstraint` | `g(x)` | `g(t, x, u, v)` | +| `ControlConstraint` | `g(u)` | `g(t, x, u, v)` | +| `MixedConstraint` | `g(x, u)` | `g(t, x, u, v)` | + +See also: [`CTBase.Data.AbstractPathConstraint`](@ref), [`CTBase.Data.StateConstraint`](@ref), +[`CTBase.Data.ControlConstraint`](@ref), [`CTBase.Data.MixedConstraint`](@ref), +[`CTBase.Traits.AbstractConstraintKind`](@ref). +""" +struct PathConstraint{F<:Function,K,TD,VD} <: AbstractPathConstraint{K,TD,VD} + f::F +end + +# ============================================================================= +# Internal keyword constructor — requires explicit constraint-kind type +# ============================================================================= + +""" +$(TYPEDSIGNATURES) + +Internal constructor for `PathConstraint` with a specific constraint-kind type and trait flags. + +# Arguments +- `f::Function`: The path-constraint function. +- `::Type{K}`: The constraint-kind trait type. +- `is_autonomous::Bool`: If true, constraint is autonomous (default: `__is_autonomous()`). +- `is_variable::Bool`: If true, constraint depends on variable (default: `__is_variable()`). + +# Returns +- `PathConstraint`: A path constraint with appropriate traits. + +See also: [`CTBase.Data.PathConstraint`](@ref), [`CTBase.Data.StateConstraint`](@ref), +[`CTBase.Data.ControlConstraint`](@ref), [`CTBase.Data.MixedConstraint`](@ref). +""" +function PathConstraint( + f, ::Type{K}; is_autonomous::Bool=__is_autonomous(), is_variable::Bool=__is_variable() +) where {K<:Traits.AbstractConstraintKind} + TD = is_autonomous ? Traits.Autonomous : Traits.NonAutonomous + VD = is_variable ? Traits.NonFixed : Traits.Fixed + return PathConstraint{typeof(f),K,TD,VD}(f) +end + +# ============================================================================= +# Typed constructor — trait types passed positionally +# ============================================================================= + +""" +$(TYPEDSIGNATURES) + +Typed constructor for `PathConstraint` with explicit trait types. + +# Arguments +- `f`: The path-constraint function. +- `::Type{K}`: The constraint-kind trait type. +- `::Type{TD}`: The time-dependence trait type. +- `::Type{VD}`: The variable-dependence trait type. + +# Returns +- `PathConstraint`: A path constraint with the specified traits. + +See also: [`CTBase.Data.PathConstraint`](@ref). +""" +function PathConstraint( + f, ::Type{K}, ::Type{TD}, ::Type{VD} +) where { + K<:Traits.AbstractConstraintKind, + TD<:Traits.TimeDependence, + VD<:Traits.VariableDependence, +} + return PathConstraint{typeof(f),K,TD,VD}(f) +end + +# ============================================================================= +# User-facing constructors +# ============================================================================= + +""" +$(TYPEDSIGNATURES) + +Construct a pure state `PathConstraint`. + +A state constraint depends on the state (and optionally time and variable), but +not on the control. + +# Arguments +- `f::Function`: The path-constraint function. +- `is_autonomous::Bool`: If true, constraint is autonomous (default: `__is_autonomous()`). +- `is_variable::Bool`: If true, constraint depends on variable (default: `__is_variable()`). + +# Example +```julia-repl +julia> using CTBase.Data + +julia> g = StateConstraint(x -> x[1]) +PathConstraint: state, autonomous, fixed (no variable) + natural call: g(x) + uniform call: g(t, x, u, v) +``` + +See also: [`CTBase.Data.ControlConstraint`](@ref), [`CTBase.Data.MixedConstraint`](@ref), +[`CTBase.Data.PathConstraint`](@ref). +""" +function StateConstraint( + f; is_autonomous::Bool=__is_autonomous(), is_variable::Bool=__is_variable() +) + return PathConstraint( + f, Traits.StateConstraintKind; is_autonomous=is_autonomous, is_variable=is_variable + ) +end + +""" +$(TYPEDSIGNATURES) + +Construct a pure control `PathConstraint`. + +A control constraint depends on the control (and optionally time and variable), +but not on the state. + +# Arguments +- `f::Function`: The path-constraint function. +- `is_autonomous::Bool`: If true, constraint is autonomous (default: `__is_autonomous()`). +- `is_variable::Bool`: If true, constraint depends on variable (default: `__is_variable()`). + +# Example +```julia-repl +julia> using CTBase.Data + +julia> g = ControlConstraint(u -> u[1]) +PathConstraint: control, autonomous, fixed (no variable) + natural call: g(u) + uniform call: g(t, x, u, v) +``` + +See also: [`CTBase.Data.StateConstraint`](@ref), [`CTBase.Data.MixedConstraint`](@ref), +[`CTBase.Data.PathConstraint`](@ref). +""" +function ControlConstraint( + f; is_autonomous::Bool=__is_autonomous(), is_variable::Bool=__is_variable() +) + return PathConstraint( + f, + Traits.ControlConstraintKind; + is_autonomous=is_autonomous, + is_variable=is_variable, + ) +end + +""" +$(TYPEDSIGNATURES) + +Construct a mixed state–control `PathConstraint`. + +A mixed constraint depends on both the state and the control (and optionally +time and variable). + +# Arguments +- `f::Function`: The path-constraint function. +- `is_autonomous::Bool`: If true, constraint is autonomous (default: `__is_autonomous()`). +- `is_variable::Bool`: If true, constraint depends on variable (default: `__is_variable()`). + +# Example +```julia-repl +julia> using CTBase.Data + +julia> g = MixedConstraint((x, u) -> x[1] + u[1]) +PathConstraint: mixed, autonomous, fixed (no variable) + natural call: g(x, u) + uniform call: g(t, x, u, v) +``` + +See also: [`CTBase.Data.StateConstraint`](@ref), [`CTBase.Data.ControlConstraint`](@ref), +[`CTBase.Data.PathConstraint`](@ref). +""" +function MixedConstraint( + f; is_autonomous::Bool=__is_autonomous(), is_variable::Bool=__is_variable() +) + return PathConstraint( + f, Traits.MixedConstraintKind; is_autonomous=is_autonomous, is_variable=is_variable + ) +end + +# ============================================================================= +# Natural call signatures — StateConstraint: g([t, ]x[, v]) +# ============================================================================= + +function (g::PathConstraint{ + <:Function,Traits.StateConstraintKind,Traits.Autonomous,Traits.Fixed +})( + x +) + return g.f(x) +end +function (g::PathConstraint{ + <:Function,Traits.StateConstraintKind,Traits.NonAutonomous,Traits.Fixed +})( + t, x +) + return g.f(t, x) +end +function (g::PathConstraint{ + <:Function,Traits.StateConstraintKind,Traits.Autonomous,Traits.NonFixed +})( + x, v +) + return g.f(x, v) +end +function (g::PathConstraint{ + <:Function,Traits.StateConstraintKind,Traits.NonAutonomous,Traits.NonFixed +})( + t, x, v +) + return g.f(t, x, v) +end + +# ============================================================================= +# Natural call signatures — ControlConstraint: g([t, ]u[, v]) +# ============================================================================= + +function (g::PathConstraint{ + <:Function,Traits.ControlConstraintKind,Traits.Autonomous,Traits.Fixed +})( + u +) + return g.f(u) +end +function (g::PathConstraint{ + <:Function,Traits.ControlConstraintKind,Traits.NonAutonomous,Traits.Fixed +})( + t, u +) + return g.f(t, u) +end +function (g::PathConstraint{ + <:Function,Traits.ControlConstraintKind,Traits.Autonomous,Traits.NonFixed +})( + u, v +) + return g.f(u, v) +end +function (g::PathConstraint{ + <:Function,Traits.ControlConstraintKind,Traits.NonAutonomous,Traits.NonFixed +})( + t, u, v +) + return g.f(t, u, v) +end + +# ============================================================================= +# Natural call signatures — MixedConstraint: g([t, ]x, u[, v]) +# ============================================================================= + +function (g::PathConstraint{ + <:Function,Traits.MixedConstraintKind,Traits.Autonomous,Traits.Fixed +})( + x, u +) + return g.f(x, u) +end +function (g::PathConstraint{ + <:Function,Traits.MixedConstraintKind,Traits.NonAutonomous,Traits.Fixed +})( + t, x, u +) + return g.f(t, x, u) +end +function (g::PathConstraint{ + <:Function,Traits.MixedConstraintKind,Traits.Autonomous,Traits.NonFixed +})( + x, u, v +) + return g.f(x, u, v) +end +function (g::PathConstraint{ + <:Function,Traits.MixedConstraintKind,Traits.NonAutonomous,Traits.NonFixed +})( + t, x, u, v +) + return g.f(t, x, u, v) +end + +# ============================================================================= +# Uniform (t, x, u, v) call — used by flow integrators. +# Every combination forwards to its natural call, ignoring unused args. +# (Mixed, NonAutonomous, NonFixed) is already covered by the natural signature. +# ============================================================================= + +# StateConstraint — uniform (t, x, u, v), no control +function (g::PathConstraint{ + <:Function,Traits.StateConstraintKind,Traits.Autonomous,Traits.Fixed +})( + _, x, _, _ +) + return g.f(x) +end +function (g::PathConstraint{ + <:Function,Traits.StateConstraintKind,Traits.NonAutonomous,Traits.Fixed +})( + t, x, _, _ +) + return g.f(t, x) +end +function (g::PathConstraint{ + <:Function,Traits.StateConstraintKind,Traits.Autonomous,Traits.NonFixed +})( + _, x, _, v +) + return g.f(x, v) +end +function (g::PathConstraint{ + <:Function,Traits.StateConstraintKind,Traits.NonAutonomous,Traits.NonFixed +})( + t, x, _, v +) + return g.f(t, x, v) +end + +# ControlConstraint — uniform (t, x, u, v), no state +function (g::PathConstraint{ + <:Function,Traits.ControlConstraintKind,Traits.Autonomous,Traits.Fixed +})( + _, _, u, _ +) + return g.f(u) +end +function (g::PathConstraint{ + <:Function,Traits.ControlConstraintKind,Traits.NonAutonomous,Traits.Fixed +})( + t, _, u, _ +) + return g.f(t, u) +end +function (g::PathConstraint{ + <:Function,Traits.ControlConstraintKind,Traits.Autonomous,Traits.NonFixed +})( + _, _, u, v +) + return g.f(u, v) +end +function (g::PathConstraint{ + <:Function,Traits.ControlConstraintKind,Traits.NonAutonomous,Traits.NonFixed +})( + t, _, u, v +) + return g.f(t, u, v) +end + +# MixedConstraint — uniform (t, x, u, v) +function (g::PathConstraint{ + <:Function,Traits.MixedConstraintKind,Traits.Autonomous,Traits.Fixed +})( + _, x, u, _ +) + return g.f(x, u) +end +function (g::PathConstraint{ + <:Function,Traits.MixedConstraintKind,Traits.NonAutonomous,Traits.Fixed +})( + t, x, u, _ +) + return g.f(t, x, u) +end +function (g::PathConstraint{ + <:Function,Traits.MixedConstraintKind,Traits.Autonomous,Traits.NonFixed +})( + _, x, u, v +) + return g.f(x, u, v) +end +# (Mixed, NonAutonomous, NonFixed) — already covered by natural 4-arg + +# ============================================================================= +# Base.show +# ============================================================================= + +""" +$(TYPEDSIGNATURES) + +Display a compact representation of a `PathConstraint` showing its traits and call signatures. + +# Arguments +- `io::IO`: The IO stream. +- `pc::PathConstraint`: The path constraint object. + +# Output +Displays three lines: +- Header with constraint-kind, time, and variable dependence traits +- Natural call signature +- Uniform call signature + +See also: [`CTBase.Data.PathConstraint`](@ref). +""" +function Base.show(io::IO, ::PathConstraint{F,K,TD,VD}) where {F,K,TD,VD} + header = "PathConstraint: $(_kind_label(K)), $(_td_label(TD)), $(_vd_label(VD))" + natural = _natural_sig_pc(K, TD, VD) + uniform = _uniform_sig_pc() + println(io, header) + println(io, " natural call: ", natural) + return print(io, " uniform call: ", uniform) +end + +""" +$(TYPEDSIGNATURES) + +Display a `PathConstraint` in the REPL with the same format as the compact `show`. + +This method is called automatically when displaying a path constraint in the Julia REPL. + +# Arguments +- `io::IO`: The IO stream. +- `mime::MIME"text/plain"`: The MIME type. +- `pc::PathConstraint`: The path constraint object. + +See also: [`CTBase.Data.PathConstraint`](@ref). +""" +function Base.show( + io::IO, ::MIME"text/plain", pc::PathConstraint{F,K,TD,VD} +) where {F,K,TD,VD} + return show(io, pc) +end diff --git a/src/Traits/Traits.jl b/src/Traits/Traits.jl index db457229..23aec353 100644 --- a/src/Traits/Traits.jl +++ b/src/Traits/Traits.jl @@ -14,6 +14,7 @@ no runtime cost. - **variable_dependence.jl**: Variable-dependence traits and the opt-in contract - **control_dependence.jl**: Control-dependence traits and the opt-in contract - **feedback.jl**: Feedback traits (`OpenLoopFeedback`, `ClosedLoopFeedback`, `DynClosedLoopFeedback`) +- **constraint_kind.jl**: Constraint-kind traits (`StateConstraintKind`, `ControlConstraintKind`, `MixedConstraintKind`) - **mutability.jl**: Mutability traits and the opt-in contract - **mode.jl**: Integration-mode traits (`EndPointMode`, `TrajectoryMode`) - **dynamics.jl**: Dynamics-type traits (`StateDynamics`, `HamiltonianDynamics`, `AugmentedHamiltonianDynamics`) @@ -29,6 +30,7 @@ no runtime cost. - **Variable dependence**: `VariableDependence`, `Fixed`, `NonFixed` - **Control dependence**: `ControlDependence`, `ControlFree`, `WithControl` - **Feedback**: `AbstractFeedback`, `OpenLoopFeedback`, `ClosedLoopFeedback`, `DynClosedLoopFeedback` +- **Constraint kind**: `AbstractConstraintKind`, `StateConstraintKind`, `ControlConstraintKind`, `MixedConstraintKind` - **Mutability**: `InPlace`, `OutOfPlace` - **Integration mode**: `EndPointMode`, `TrajectoryMode` - **Dynamics**: `StateDynamics`, `HamiltonianDynamics`, `AugmentedHamiltonianDynamics` @@ -86,6 +88,7 @@ include(joinpath(@__DIR__, "time_dependence.jl")) include(joinpath(@__DIR__, "variable_dependence.jl")) include(joinpath(@__DIR__, "control_dependence.jl")) include(joinpath(@__DIR__, "feedback.jl")) +include(joinpath(@__DIR__, "constraint_kind.jl")) # ============================================================================== # Module exports @@ -105,8 +108,11 @@ export SupportsVariableCostate, NoVariableCostate export VariableDependence, Fixed, NonFixed export ControlDependence, ControlFree, WithControl export AbstractFeedback, OpenLoopFeedback, ClosedLoopFeedback, DynClosedLoopFeedback -export ad_trait, variable_costate_trait, dynamics_trait, feedback +export AbstractConstraintKind, + StateConstraintKind, ControlConstraintKind, MixedConstraintKind +export ad_trait, variable_costate_trait, dynamics_trait, feedback, constraint_kind export is_open_loop, is_closed_loop, is_dyn_closed_loop +export is_state_constraint, is_control_constraint, is_mixed_constraint export is_inplace, is_outofplace export is_autonomous, is_nonautonomous, is_variable, is_nonvariable, has_variable export is_control_free, has_control diff --git a/src/Traits/constraint_kind.jl b/src/Traits/constraint_kind.jl new file mode 100644 index 00000000..b6c453b5 --- /dev/null +++ b/src/Traits/constraint_kind.jl @@ -0,0 +1,117 @@ +""" +$(TYPEDEF) + +Abstract base type for constraint-kind traits. + +Constraint-kind traits encode, at the type level, which primal variables a path +constraint `g(...)` depends on. They distinguish between pure state constraints, +pure control constraints, and mixed state–control constraints. + +# Trait Pattern + +This trait follows the **type-parameter-only** contract (like +[`CTBase.Traits.AbstractFeedback`](@ref)): the trait value is read from a type parameter +of the concrete data type (e.g. `PathConstraint{F,K,TD,VD}`) by the `constraint_kind` +accessor. No `has_constraint_kind_trait` guard is provided; calling `constraint_kind` +on a type that does not implement it yields a standard `MethodError`. + +See also: [`CTBase.Traits.StateConstraintKind`](@ref), [`CTBase.Traits.ControlConstraintKind`](@ref), +[`CTBase.Traits.MixedConstraintKind`](@ref), [`CTBase.Traits.constraint_kind`](@ref). +""" +abstract type AbstractConstraintKind <: AbstractTrait end + +""" +$(TYPEDEF) + +Trait indicating a pure state path constraint: `g` depends on the state (and +optionally time and variable), but not on the control. + +A state constraint has the form `g(x)` (or `g(t, x)`, `g(x, v)`, `g(t, x, v)`). + +See also: [`CTBase.Traits.ControlConstraintKind`](@ref), [`CTBase.Traits.MixedConstraintKind`](@ref), +[`CTBase.Traits.AbstractConstraintKind`](@ref). +""" +struct StateConstraintKind <: AbstractConstraintKind end + +""" +$(TYPEDEF) + +Trait indicating a pure control path constraint: `g` depends on the control (and +optionally time and variable), but not on the state. + +A control constraint has the form `g(u)` (or `g(t, u)`, `g(u, v)`, `g(t, u, v)`). + +See also: [`CTBase.Traits.StateConstraintKind`](@ref), [`CTBase.Traits.MixedConstraintKind`](@ref), +[`CTBase.Traits.AbstractConstraintKind`](@ref). +""" +struct ControlConstraintKind <: AbstractConstraintKind end + +""" +$(TYPEDEF) + +Trait indicating a mixed state–control path constraint: `g` depends on both the +state and the control (and optionally time and variable). + +A mixed constraint has the form `g(x, u)` (or `g(t, x, u)`, `g(x, u, v)`, +`g(t, x, u, v)`). + +See also: [`CTBase.Traits.StateConstraintKind`](@ref), [`CTBase.Traits.ControlConstraintKind`](@ref), +[`CTBase.Traits.AbstractConstraintKind`](@ref). +""" +struct MixedConstraintKind <: AbstractConstraintKind end + +""" +$(TYPEDSIGNATURES) + +Return the constraint-kind trait of `x`. + +Methods are defined on concrete types in `Data` (e.g. `AbstractPathConstraint`). +The trait value is one of [`CTBase.Traits.StateConstraintKind`](@ref), +[`CTBase.Traits.ControlConstraintKind`](@ref), or [`CTBase.Traits.MixedConstraintKind`](@ref). + +See also: [`CTBase.Traits.AbstractConstraintKind`](@ref), [`CTBase.Traits.StateConstraintKind`](@ref), +[`CTBase.Traits.ControlConstraintKind`](@ref), [`CTBase.Traits.MixedConstraintKind`](@ref). +""" +function constraint_kind end + +""" +$(TYPEDSIGNATURES) + +Return `true` if `x` is a pure state path constraint. + +Methods are defined on concrete types in `Data` (e.g. `AbstractPathConstraint`). + +# Returns +- `Bool`: `true` if the constraint-kind trait is `StateConstraintKind`, `false` otherwise. + +See also: [`CTBase.Traits.StateConstraintKind`](@ref), [`CTBase.Traits.constraint_kind`](@ref). +""" +function is_state_constraint end + +""" +$(TYPEDSIGNATURES) + +Return `true` if `x` is a pure control path constraint. + +Methods are defined on concrete types in `Data` (e.g. `AbstractPathConstraint`). + +# Returns +- `Bool`: `true` if the constraint-kind trait is `ControlConstraintKind`, `false` otherwise. + +See also: [`CTBase.Traits.ControlConstraintKind`](@ref), [`CTBase.Traits.constraint_kind`](@ref). +""" +function is_control_constraint end + +""" +$(TYPEDSIGNATURES) + +Return `true` if `x` is a mixed state–control path constraint. + +Methods are defined on concrete types in `Data` (e.g. `AbstractPathConstraint`). + +# Returns +- `Bool`: `true` if the constraint-kind trait is `MixedConstraintKind`, `false` otherwise. + +See also: [`CTBase.Traits.MixedConstraintKind`](@ref), [`CTBase.Traits.constraint_kind`](@ref). +""" +function is_mixed_constraint end diff --git a/test/suite/data/test_abstract_multiplier.jl b/test/suite/data/test_abstract_multiplier.jl new file mode 100644 index 00000000..142ffdd3 --- /dev/null +++ b/test/suite/data/test_abstract_multiplier.jl @@ -0,0 +1,80 @@ +module TestAbstractMultiplier + +using Test: Test +import CTBase.Data +import CTBase.Traits + +const VERBOSE = isdefined(Main, :TestData) ? Main.TestData.VERBOSE : true +const SHOWTIMING = isdefined(Main, :TestData) ? Main.TestData.SHOWTIMING : true + +# ============================================================================== +# Fake type for contract testing (defined at module top-level per testing-creation.md) +# ============================================================================== + +struct FakeMultiplier{TD,VD} <: Data.AbstractMultiplier{TD,VD} end + +# ============================================================================== +# Test function +# ============================================================================== + +function test_abstract_multiplier() + Test.@testset "AbstractMultiplier Tests" verbose = VERBOSE showtiming = SHOWTIMING begin + + # ==================================================================== + # UNIT TESTS - Abstract Type Definition + # ==================================================================== + + Test.@testset "Abstract Type Definition" begin + Test.@test isdefined(Data, :AbstractMultiplier) + Test.@test isabstracttype(Data.AbstractMultiplier) + + fake = FakeMultiplier{Traits.Autonomous,Traits.Fixed}() + Test.@test fake isa Data.AbstractMultiplier + end + + # ==================================================================== + # UNIT TESTS - Trait Accessors on Abstract Type + # ==================================================================== + + Test.@testset "Trait Accessors on Abstract Type" begin + Test.@testset "has_*_trait return true" begin + fake = FakeMultiplier{Traits.Autonomous,Traits.Fixed}() + Test.@test Traits.has_time_dependence_trait(fake) === true + Test.@test Traits.has_variable_dependence_trait(fake) === true + end + + Test.@testset "time/variable dependence return correct trait" begin + fake_aut = FakeMultiplier{Traits.Autonomous,Traits.Fixed}() + fake_nonaut = FakeMultiplier{Traits.NonAutonomous,Traits.NonFixed}() + Test.@test Traits.time_dependence(fake_aut) === Traits.Autonomous + Test.@test Traits.time_dependence(fake_nonaut) === Traits.NonAutonomous + Test.@test Traits.variable_dependence(fake_aut) === Traits.Fixed + Test.@test Traits.variable_dependence(fake_nonaut) === Traits.NonFixed + end + end + + # ==================================================================== + # UNIT TESTS - Type Stability + # ==================================================================== + + Test.@testset "Type Stability" begin + fake = FakeMultiplier{Traits.Autonomous,Traits.Fixed}() + Test.@test Test.@inferred(Traits.time_dependence(fake)) === Traits.Autonomous + Test.@test Test.@inferred(Traits.variable_dependence(fake)) === Traits.Fixed + end + + # ==================================================================== + # UNIT TESTS - Liskov Substitution + # ==================================================================== + + Test.@testset "Liskov Substitution" begin + m = Data.Multiplier((x, p) -> x[1]) + Test.@test m isa Data.AbstractMultiplier + end + end +end + +end # module + +# CRITICAL: Redefine in outer scope for TestRunner +test_abstract_multiplier() = TestAbstractMultiplier.test_abstract_multiplier() diff --git a/test/suite/data/test_abstract_path_constraint.jl b/test/suite/data/test_abstract_path_constraint.jl new file mode 100644 index 00000000..98b31e5c --- /dev/null +++ b/test/suite/data/test_abstract_path_constraint.jl @@ -0,0 +1,123 @@ +module TestAbstractPathConstraint + +using Test: Test +import CTBase.Data +import CTBase.Traits + +const VERBOSE = isdefined(Main, :TestData) ? Main.TestData.VERBOSE : true +const SHOWTIMING = isdefined(Main, :TestData) ? Main.TestData.SHOWTIMING : true + +# ============================================================================== +# Fake type for contract testing (defined at module top-level per testing-creation.md) +# ============================================================================== + +struct FakePathConstraint{K,TD,VD} <: Data.AbstractPathConstraint{K,TD,VD} end + +# ============================================================================== +# Test function +# ============================================================================== + +function test_abstract_path_constraint() + Test.@testset "AbstractPathConstraint Tests" verbose = VERBOSE showtiming = SHOWTIMING begin + + # ==================================================================== + # UNIT TESTS - Abstract Type Definition + # ==================================================================== + + Test.@testset "Abstract Type Definition" begin + Test.@test isdefined(Data, :AbstractPathConstraint) + Test.@test isabstracttype(Data.AbstractPathConstraint) + + fake = FakePathConstraint{ + Traits.StateConstraintKind,Traits.Autonomous,Traits.Fixed + }() + Test.@test fake isa Data.AbstractPathConstraint + end + + # ==================================================================== + # UNIT TESTS - Trait Accessors on Abstract Type + # ==================================================================== + + Test.@testset "Trait Accessors on Abstract Type" begin + Test.@testset "has_*_trait return true" begin + fake = FakePathConstraint{ + Traits.StateConstraintKind,Traits.Autonomous,Traits.Fixed + }() + Test.@test Traits.has_time_dependence_trait(fake) === true + Test.@test Traits.has_variable_dependence_trait(fake) === true + end + + Test.@testset "constraint_kind returns correct trait" begin + fs = FakePathConstraint{ + Traits.StateConstraintKind,Traits.Autonomous,Traits.Fixed + }() + fc = FakePathConstraint{ + Traits.ControlConstraintKind,Traits.Autonomous,Traits.Fixed + }() + fm = FakePathConstraint{ + Traits.MixedConstraintKind,Traits.Autonomous,Traits.Fixed + }() + Test.@test Traits.constraint_kind(fs) === Traits.StateConstraintKind + Test.@test Traits.constraint_kind(fc) === Traits.ControlConstraintKind + Test.@test Traits.constraint_kind(fm) === Traits.MixedConstraintKind + end + + Test.@testset "time/variable dependence return correct trait" begin + fake = FakePathConstraint{ + Traits.MixedConstraintKind,Traits.NonAutonomous,Traits.NonFixed + }() + Test.@test Traits.time_dependence(fake) === Traits.NonAutonomous + Test.@test Traits.variable_dependence(fake) === Traits.NonFixed + end + + Test.@testset "predicates dispatch on the K parameter" begin + fs = FakePathConstraint{ + Traits.StateConstraintKind,Traits.Autonomous,Traits.Fixed + }() + fc = FakePathConstraint{ + Traits.ControlConstraintKind,Traits.Autonomous,Traits.Fixed + }() + fm = FakePathConstraint{ + Traits.MixedConstraintKind,Traits.Autonomous,Traits.Fixed + }() + Test.@test Traits.is_state_constraint(fs) + Test.@test !Traits.is_control_constraint(fs) + Test.@test !Traits.is_mixed_constraint(fs) + + Test.@test Traits.is_control_constraint(fc) + Test.@test !Traits.is_state_constraint(fc) + + Test.@test Traits.is_mixed_constraint(fm) + Test.@test !Traits.is_state_constraint(fm) + end + end + + # ==================================================================== + # UNIT TESTS - Type Stability + # ==================================================================== + + Test.@testset "Type Stability" begin + fake = FakePathConstraint{ + Traits.StateConstraintKind,Traits.Autonomous,Traits.Fixed + }() + Test.@test Test.@inferred(Traits.constraint_kind(fake)) === + Traits.StateConstraintKind + Test.@test Test.@inferred(Traits.time_dependence(fake)) === Traits.Autonomous + Test.@test Test.@inferred(Traits.variable_dependence(fake)) === Traits.Fixed + end + + # ==================================================================== + # UNIT TESTS - Liskov Substitution + # ==================================================================== + + Test.@testset "Liskov Substitution" begin + g = Data.StateConstraint(x -> x[1]) + Test.@test g isa Data.AbstractPathConstraint + end + end +end + +end # module + +# CRITICAL: Redefine in outer scope for TestRunner +test_abstract_path_constraint() = TestAbstractPathConstraint.test_abstract_path_constraint() diff --git a/test/suite/data/test_data_module.jl b/test/suite/data/test_data_module.jl index 388466d5..b8f92e42 100644 --- a/test/suite/data/test_data_module.jl +++ b/test/suite/data/test_data_module.jl @@ -39,6 +39,8 @@ const EXPORTED_ABSTRACT_TYPES = ( :AbstractHamiltonian, :AbstractControlLaw, :AbstractPseudoHamiltonian, + :AbstractPathConstraint, + :AbstractMultiplier, ) const EXPORTED_CONCRETE_TYPES = ( @@ -50,6 +52,11 @@ const EXPORTED_CONCRETE_TYPES = ( :ClosedLoop, :DynClosedLoop, :PseudoHamiltonian, + :PathConstraint, + :StateConstraint, + :ControlConstraint, + :MixedConstraint, + :Multiplier, ) const PRIVATE_SYMBOLS = ( @@ -74,6 +81,12 @@ const PRIVATE_SYMBOLS = ( :_uniform_sig_cl, :_natural_sig_ph, :_uniform_sig_ph, + :_kind_label, + :_natural_sig_pc, + :_natural_args_pc, + :_uniform_sig_pc, + :_natural_sig_mult, + :_uniform_sig_mult, ) # ============================================================================ diff --git a/test/suite/data/test_multiplier.jl b/test/suite/data/test_multiplier.jl new file mode 100644 index 00000000..945496fe --- /dev/null +++ b/test/suite/data/test_multiplier.jl @@ -0,0 +1,109 @@ +module TestMultiplier + +using Test: Test +import CTBase.Data: Data, Multiplier +import CTBase.Traits + +const VERBOSE = isdefined(Main, :TestData) ? Main.TestData.VERBOSE : true +const SHOWTIMING = isdefined(Main, :TestData) ? Main.TestData.SHOWTIMING : true + +function test_multiplier() + Test.@testset "Multiplier Tests" verbose = VERBOSE showtiming = SHOWTIMING begin + + # ==================================================================== + # UNIT TESTS - Construction with all trait combinations + # ==================================================================== + + Test.@testset "Unit: construction and trait extractors" begin + m1 = Multiplier((x, p) -> x[1]; is_autonomous=true, is_variable=false) + Test.@test m1 isa Data.Multiplier + Test.@test m1 isa Data.AbstractMultiplier + Test.@test Traits.time_dependence(m1) === Traits.Autonomous + Test.@test Traits.variable_dependence(m1) === Traits.Fixed + + m2 = Multiplier( + (t, x, p, v) -> t + x[1] + p[1] + v[1]; + is_autonomous=false, + is_variable=true, + ) + Test.@test Traits.time_dependence(m2) === Traits.NonAutonomous + Test.@test Traits.variable_dependence(m2) === Traits.NonFixed + end + + # ==================================================================== + # UNIT TESTS - Natural calls, all trait combos + # ==================================================================== + + Test.@testset "Natural calls" begin + Test.@test Multiplier((x, p) -> x[1] * p[1])([2.0], [3.0]) == 6.0 + Test.@test Multiplier((t, x, p) -> t + x[1] + p[1]; is_autonomous=false)( + 1.0, [2.0], [3.0] + ) == 6.0 + Test.@test Multiplier((x, p, v) -> x[1] + p[1] + v[1]; is_variable=true)( + [2.0], [3.0], [4.0] + ) == 9.0 + Test.@test Multiplier( + (t, x, p, v) -> t + x[1] + p[1] + v[1]; + is_autonomous=false, + is_variable=true, + )( + 1.0, [2.0], [3.0], [4.0] + ) == 10.0 + end + + # ==================================================================== + # UNIT TESTS - Uniform call μ(t, x, p, v) + # ==================================================================== + + Test.@testset "Uniform call μ(t, x, p, v)" begin + t, x, p, v = 1.0, [2.0], [3.0], [4.0] + Test.@test Multiplier((x, p) -> x[1] + p[1])(t, x, p, v) == 5.0 + Test.@test Multiplier((t, x, p) -> t + x[1] + p[1]; is_autonomous=false)( + t, x, p, v + ) == 6.0 + Test.@test Multiplier((x, p, v) -> x[1] + p[1] + v[1]; is_variable=true)( + t, x, p, v + ) == 9.0 + # NonAut NonFixed: natural == uniform + Test.@test Multiplier( + (t, x, p, v) -> t + x[1] + p[1] + v[1]; + is_autonomous=false, + is_variable=true, + )( + t, x, p, v + ) == 10.0 + end + + # ==================================================================== + # UNIT TESTS - Typed constructor + # ==================================================================== + + Test.@testset "Typed constructor" begin + m = Multiplier((x, p) -> x[1], Traits.Autonomous, Traits.Fixed) + Test.@test m isa Data.AbstractMultiplier + Test.@test m([5.0], [0.0]) == 5.0 + end + + # ==================================================================== + # UNIT TESTS - show + # ==================================================================== + + Test.@testset "show" begin + m = Multiplier((x, p) -> x[1]) + str = repr(MIME("text/plain"), m) + Test.@test occursin("Multiplier: autonomous, fixed (no variable)", str) + Test.@test occursin("natural call: μ(x, p)", str) + Test.@test occursin("uniform call: μ(t, x, p, v)", str) + + m2 = Multiplier((t, x, p) -> x[1]; is_autonomous=false) + str2 = repr(MIME("text/plain"), m2) + Test.@test occursin("Multiplier: non-autonomous, fixed (no variable)", str2) + Test.@test occursin("natural call: μ(t, x, p)", str2) + end + end +end + +end # module + +# CRITICAL: Redefine in outer scope for TestRunner +test_multiplier() = TestMultiplier.test_multiplier() diff --git a/test/suite/data/test_path_constraint.jl b/test/suite/data/test_path_constraint.jl new file mode 100644 index 00000000..023f1187 --- /dev/null +++ b/test/suite/data/test_path_constraint.jl @@ -0,0 +1,183 @@ +module TestPathConstraint + +using Test: Test +import CTBase.Data: Data, PathConstraint, StateConstraint, ControlConstraint, MixedConstraint +import CTBase.Traits + +const VERBOSE = isdefined(Main, :TestData) ? Main.TestData.VERBOSE : true +const SHOWTIMING = isdefined(Main, :TestData) ? Main.TestData.SHOWTIMING : true + +function test_path_constraint() + Test.@testset "PathConstraint Tests" verbose = VERBOSE showtiming = SHOWTIMING begin + + # ==================================================================== + # UNIT TESTS - Construction with all trait combinations + # ==================================================================== + + Test.@testset "Unit: construction and trait extractors" begin + # StateConstraint, Autonomous, Fixed + g1 = StateConstraint(x -> x[1]; is_autonomous=true, is_variable=false) + Test.@test g1 isa Data.PathConstraint + Test.@test Traits.constraint_kind(g1) === Traits.StateConstraintKind + Test.@test Traits.time_dependence(g1) === Traits.Autonomous + Test.@test Traits.variable_dependence(g1) === Traits.Fixed + Test.@test Traits.is_state_constraint(g1) + Test.@test !Traits.is_control_constraint(g1) + Test.@test !Traits.is_mixed_constraint(g1) + + # ControlConstraint, NonAutonomous, NonFixed + g2 = ControlConstraint( + (t, u, v) -> u[1] + t + v[1]; is_autonomous=false, is_variable=true + ) + Test.@test Traits.constraint_kind(g2) === Traits.ControlConstraintKind + Test.@test Traits.time_dependence(g2) === Traits.NonAutonomous + Test.@test Traits.variable_dependence(g2) === Traits.NonFixed + Test.@test Traits.is_control_constraint(g2) + + # MixedConstraint, Autonomous, Fixed + g3 = MixedConstraint((x, u) -> x[1] + u[1]) + Test.@test Traits.constraint_kind(g3) === Traits.MixedConstraintKind + Test.@test Traits.is_mixed_constraint(g3) + end + + # ==================================================================== + # UNIT TESTS - Natural calls, all kinds × trait combos + # ==================================================================== + + Test.@testset "Natural calls - StateConstraint" begin + Test.@test StateConstraint(x -> 2x[1])([3.0]) == 6.0 + Test.@test StateConstraint((t, x) -> t + x[1]; is_autonomous=false)(1.0, [2.0]) == + 3.0 + Test.@test StateConstraint((x, v) -> x[1] * v[1]; is_variable=true)( + [2.0], [5.0] + ) == 10.0 + Test.@test StateConstraint( + (t, x, v) -> t + x[1] + v[1]; is_autonomous=false, is_variable=true + )( + 1.0, [2.0], [3.0] + ) == 6.0 + end + + Test.@testset "Natural calls - ControlConstraint" begin + Test.@test ControlConstraint(u -> 2u[1])([3.0]) == 6.0 + Test.@test ControlConstraint((t, u) -> t + u[1]; is_autonomous=false)( + 1.0, [2.0] + ) == 3.0 + Test.@test ControlConstraint((u, v) -> u[1] * v[1]; is_variable=true)( + [2.0], [5.0] + ) == 10.0 + end + + Test.@testset "Natural calls - MixedConstraint" begin + Test.@test MixedConstraint((x, u) -> x[1] + u[1])([2.0], [3.0]) == 5.0 + Test.@test MixedConstraint( + (t, x, u) -> t + x[1] + u[1]; is_autonomous=false + )( + 1.0, [2.0], [3.0] + ) == 6.0 + Test.@test MixedConstraint( + (t, x, u, v) -> t + x[1] + u[1] + v[1]; + is_autonomous=false, + is_variable=true, + )( + 1.0, [2.0], [3.0], [4.0] + ) == 10.0 + end + + # ==================================================================== + # UNIT TESTS - Uniform call g(t, x, u, v) + # ==================================================================== + + Test.@testset "Uniform call g(t, x, u, v)" begin + t, x, u, v = 1.0, [2.0], [3.0], [4.0] + + # State: ignores u + Test.@test StateConstraint(x -> x[1])(t, x, u, v) == 2.0 + Test.@test StateConstraint((t, x) -> t + x[1]; is_autonomous=false)( + t, x, u, v + ) == 3.0 + Test.@test StateConstraint((x, v) -> x[1] + v[1]; is_variable=true)( + t, x, u, v + ) == 6.0 + Test.@test StateConstraint( + (t, x, v) -> t + x[1] + v[1]; is_autonomous=false, is_variable=true + )( + t, x, u, v + ) == 7.0 + + # Control: ignores x + Test.@test ControlConstraint(u -> u[1])(t, x, u, v) == 3.0 + Test.@test ControlConstraint((t, u) -> t + u[1]; is_autonomous=false)( + t, x, u, v + ) == 4.0 + Test.@test ControlConstraint((u, v) -> u[1] + v[1]; is_variable=true)( + t, x, u, v + ) == 7.0 + + # Mixed + Test.@test MixedConstraint((x, u) -> x[1] + u[1])(t, x, u, v) == 5.0 + Test.@test MixedConstraint((t, x, u) -> t + x[1] + u[1]; is_autonomous=false)( + t, x, u, v + ) == 6.0 + Test.@test MixedConstraint((x, u, v) -> x[1] + u[1] + v[1]; is_variable=true)( + t, x, u, v + ) == 9.0 + # Mixed NonAut NonFixed: natural == uniform + Test.@test MixedConstraint( + (t, x, u, v) -> t + x[1] + u[1] + v[1]; + is_autonomous=false, + is_variable=true, + )( + t, x, u, v + ) == 10.0 + end + + # ==================================================================== + # UNIT TESTS - Typed constructor + # ==================================================================== + + Test.@testset "Typed constructor" begin + g = PathConstraint( + x -> x[1], + Traits.StateConstraintKind, + Traits.Autonomous, + Traits.Fixed, + ) + Test.@test g isa Data.AbstractPathConstraint + Test.@test Traits.constraint_kind(g) === Traits.StateConstraintKind + Test.@test g([5.0]) == 5.0 + end + + # ==================================================================== + # UNIT TESTS - Subtyping + # ==================================================================== + + Test.@testset "Subtyping" begin + g = StateConstraint(x -> x[1]) + Test.@test g isa Data.AbstractPathConstraint + Test.@test typeof(g) <: Data.PathConstraint + end + + # ==================================================================== + # UNIT TESTS - show + # ==================================================================== + + Test.@testset "show" begin + g = MixedConstraint((x, u) -> x[1] + u[1]) + str = repr(MIME("text/plain"), g) + Test.@test occursin("PathConstraint: mixed, autonomous, fixed (no variable)", str) + Test.@test occursin("natural call: g(x, u)", str) + Test.@test occursin("uniform call: g(t, x, u, v)", str) + + gs = StateConstraint((t, x, v) -> x[1]; is_autonomous=false, is_variable=true) + strs = repr(MIME("text/plain"), gs) + Test.@test occursin("PathConstraint: state, non-autonomous, variable", strs) + Test.@test occursin("natural call: g(t, x, v)", strs) + end + end +end + +end # module + +# CRITICAL: Redefine in outer scope for TestRunner +test_path_constraint() = TestPathConstraint.test_path_constraint() diff --git a/test/suite/traits/test_constraint_kind.jl b/test/suite/traits/test_constraint_kind.jl new file mode 100644 index 00000000..7216062e --- /dev/null +++ b/test/suite/traits/test_constraint_kind.jl @@ -0,0 +1,89 @@ +module TestConstraintKind + +using Test: Test +import CTBase.Traits + +const VERBOSE = isdefined(Main, :TestTraits) ? Main.TestTraits.VERBOSE : true +const SHOWTIMING = isdefined(Main, :TestTraits) ? Main.TestTraits.SHOWTIMING : true + +function test_constraint_kind() + Test.@testset "Constraint-Kind Trait Tests" verbose = VERBOSE showtiming = SHOWTIMING begin + + # ==================================================================== + # UNIT TESTS - Abstract Type + # ==================================================================== + + Test.@testset "UNIT TESTS - Abstract Type" begin + Test.@test isdefined(Traits, :AbstractConstraintKind) + Test.@test isabstracttype(Traits.AbstractConstraintKind) + Test.@test Traits.AbstractConstraintKind <: Traits.AbstractTrait + end + + # ==================================================================== + # UNIT TESTS - Concrete Trait Types + # ==================================================================== + + Test.@testset "UNIT TESTS - Concrete Trait Types" begin + for K in ( + Traits.StateConstraintKind, + Traits.ControlConstraintKind, + Traits.MixedConstraintKind, + ) + Test.@testset "$(K)" begin + Test.@test isdefined(Traits, nameof(K)) + Test.@test !isabstracttype(K) + Test.@test K() isa K + Test.@test K <: Traits.AbstractConstraintKind + Test.@test K <: Traits.AbstractTrait + end + end + end + + # ==================================================================== + # UNIT TESTS - Type Hierarchy + # ==================================================================== + + Test.@testset "UNIT TESTS - Type Hierarchy" begin + Test.@testset "Distinct from feedback traits" begin + Test.@test !(Traits.StateConstraintKind <: Traits.AbstractFeedback) + Test.@test !(Traits.ControlConstraintKind <: Traits.AbstractFeedback) + Test.@test !(Traits.MixedConstraintKind <: Traits.AbstractFeedback) + end + end + + # ==================================================================== + # UNIT TESTS - accessor and predicates + # ==================================================================== + + Test.@testset "UNIT TESTS - accessor and predicates" begin + Test.@test isdefined(Traits, :constraint_kind) + Test.@test isa(Traits.constraint_kind, Function) + Test.@test isdefined(Traits, :is_state_constraint) + Test.@test isdefined(Traits, :is_control_constraint) + Test.@test isdefined(Traits, :is_mixed_constraint) + end + + # ==================================================================== + # Exports Verification + # ==================================================================== + + Test.@testset "Exports Verification" begin + for sym in ( + :AbstractConstraintKind, + :StateConstraintKind, + :ControlConstraintKind, + :MixedConstraintKind, + :constraint_kind, + :is_state_constraint, + :is_control_constraint, + :is_mixed_constraint, + ) + Test.@test isdefined(Traits, sym) + end + end + end +end + +end # module + +test_constraint_kind() = TestConstraintKind.test_constraint_kind()