CTBase.jl
The CTBase.jl package is part of the control-toolbox ecosystem.
The root package is OptimalControl.jl which aims to provide tools to model and solve optimal control problems with ordinary differential equations by direct and indirect methods, both on CPU and GPU.
In some examples in the documentation, private methods are shown without the module prefix. This is done for the sake of clarity and readability.
julia> using CTBase
julia> x = 1
julia> private_fun(x) # throws an errorThis should instead be written as:
julia> using CTBase
julia> x = 1
julia> CTBase.private_fun(x)If the method is re-exported by another package,
module OptimalControl
import CTBase: private_fun
export private_fun
endthen there is no need to prefix it with the original module name:
julia> using OptimalControl
julia> x = 1
julia> private_fun(x)Descriptions: encoding algorithms
One of the central ideas in CTBase is the notion of a description. A description is simply a tuple of Symbols that encodes an algorithm or configuration in a declarative way.
Formally, CTBase defines:
const DescVarArg = Vararg{Symbol}
const Description = Tuple{DescVarArg}For example, the tuple
julia> using CTBase
julia> d = (:descent, :bfgs, :bisection)
(:descent, :bfgs, :bisection)
julia> typeof(d) <: CTBase.Description
truecan be read as “a descent algorithm, with BFGS directions and a bisection line search”. Higher-level packages in the control-toolbox ecosystem use descriptions to catalogue algorithms in a uniform way.
Building a library of descriptions
CTBase provides a few small functions to manage collections of descriptions:
CTBase.add(x, y)adds the descriptionyto the tuple of descriptionsx, rejecting duplicates with anIncorrectArgumentexception.CTBase.complete(list; descriptions=D)picks a complete description from a setDbased on a partial list of symbols.CTBase.remove(x, y)returns the set difference of two descriptions.
Here is a complete example of a small “algorithm library”:
julia> algorithms = ()
()
julia> algorithms = CTBase.add(algorithms, (:descent, :bfgs, :bisection))
((:descent, :bfgs, :bisection),)
julia> algorithms = CTBase.add(algorithms, (:descent, :gradient, :fixedstep))
((:descent, :bfgs, :bisection), (:descent, :gradient, :fixedstep))
julia> display(algorithms)
(:descent, :bfgs, :bisection)
(:descent, :gradient, :fixedstep)Given this library, we can complete a partial description:
julia> CTBase.complete((:descent,); descriptions=algorithms)
(:descent, :bfgs, :bisection)
julia> CTBase.complete((:gradient, :fixedstep); descriptions=algorithms)
(:descent, :gradient, :fixedstep)Internally, CTBase.complete scans the descriptions tuple from top to bottom. For each candidate description it computes:
- how many symbols it shares with the partial list, and
- whether the partial list is a subset of the full description.
If no description contains all the symbols from the partial list, AmbiguousDescription is thrown. Otherwise, among the descriptions that do contain the partial list, CTBase selects the one with the largest intersection; if several have the same score, the first one in the descriptions tuple wins. In other words, the order of descriptions encodes a priority from top to bottom.
With this mechanism in place, we can then analyse the remainder of a description by removing a prefix:
julia> full = CTBase.complete((:descent,); descriptions=algorithms)
(:descent, :bfgs, :bisection)
julia> CTBase.remove(full, (:descent, :bfgs))
(:bisection,)This “description language” lets higher-level packages refer to algorithms in a structured, composable way, while CTBase takes care of the low-level operations (adding, completing, and comparing descriptions).
Error handling and CTBase exceptions
CTBase defines a small hierarchy of domain-specific exceptions to make error handling explicit and consistent across the control-toolbox ecosystem.
All custom exceptions inherit from CTBase.CTException:
abstract type CTBase.CTException <: Exception endYou should generally catch exceptions like this:
try
# call into CTBase or a package built on top of it
catch e
if e isa CTBase.CTException
# handle CTBase domain errors in a uniform way
@warn "CTBase error" exception=(e, catch_backtrace())
else
# non-CTBase error: rethrow so it is not hidden
rethrow()
end
endThis pattern avoids accidentally swallowing unrelated internal errors while still giving you a single place to handle all CTBase-specific problems.
AmbiguousDescription
CTBase.AmbiguousDescription <: CTBase.CTExceptionThrown when a description (a tuple of Symbols) cannot be matched to any known valid description. This typically happens in CTBase.complete when the user provides an incomplete or inconsistent description.
julia> using CTBase
julia> D = ((:a, :b), (:a, :b, :c), (:b, :c))
julia> CTBase.complete(:f; descriptions=D)
ERROR: AmbiguousDescription: the description (:f,) is ambiguous / incorrectUse this exception when the high-level choice of description itself is wrong or ambiguous and there is no sensible default.
IncorrectArgument
CTBase.IncorrectArgument <: CTBase.CTExceptionThrown when an individual argument is invalid or violates a precondition.
Examples from CTBase:
Adding a duplicate description:
julia> algorithms = CTBase.add((), (:a, :b)) julia> CTBase.add(algorithms, (:a, :b)) ERROR: IncorrectArgument: the description (:a, :b) is already in ((:a, :b),)Using invalid indices for the Unicode helpers:
julia> CTBase.ctindice(-1) ERROR: IncorrectArgument: the subscript must be between 0 and 9
Use this exception whenever one input value is outside the allowed domain (wrong range, duplicate, empty when it must not be, etc.).
NotImplemented
CTBase.NotImplemented <: CTBase.CTExceptionUsed to mark interface points that must be implemented by concrete subtypes. The typical pattern is to provide a method on an abstract type that throws NotImplemented, and then override it in each concrete implementation:
abstract type MyAbstractAlgorithm end
function run!(algo::MyAbstractAlgorithm, state)
throw(CTBase.NotImplemented("run! is not implemented for $(typeof(algo))"))
endConcrete algorithms then provide their own run! method instead of raising this exception. This makes it easy to detect missing implementations during testing.
Use NotImplemented when defining interfaces and you want an explicit, typed error rather than a generic error("TODO").
UnauthorizedCall
CTBase.UnauthorizedCall <: CTBase.CTExceptionSignals that a function call is not allowed in the current state of the object or system. This is different from IncorrectArgument: here the arguments may be valid, but the call is forbidden because of when or how it is made.
A common pattern is a method that is meant to be called only once:
function finalize!(s::SomeState)
if s.is_finalized
throw(CTBase.UnauthorizedCall("finalize! was already called for this state"))
end
# ... perform finalisation and mark state as finalised ...
endUse UnauthorizedCall when the calling context is invalid (wrong phase of a computation, method already called, state already closed, missing permissions, illegal order of calls, etc.).
It is also used internally by ExtensionError when it is called without any weak dependencies:
julia> using CTBase
julia> CTBase.ExtensionError()
ERROR: UnauthorizedCall: Please provide at least one weak dependence for the extension.ParsingError
CTBase.ParsingError <: CTBase.CTExceptionIntended for errors detected during parsing of input structures or DSLs (domain-specific languages).
julia> using CTBase
julia> throw(CTBase.ParsingError("unexpected token 'end'"))
ERROR: ParsingError: unexpected token 'end'