Options System
This guide explains the Options module — the foundational layer for defining, validating, extracting, and tracking configuration values throughout CTBase. The Options module is generic and has no dependencies on other CTBase modules.
using CTBaseOverview
The options system has four core types and a set of extraction functions:
OptionDefinition (schema)
├─► StrategyMetadata (collection of definitions)
│ └─► build_strategy_options (validate + merge)
│ └─► StrategyOptions (validated values)
└─► extract_option (single extraction)
└─► OptionValue (value + provenance)OptionDefinition
An OptionDefinition is the schema for a single option. It specifies the name, type, default, description, aliases, and an optional validator.
def = OptionDefinition(
name = :max_iter,
type = Integer,
default = 1000,
description = "Maximum number of iterations",
aliases = (:maxiter,),
validator = x -> x >= 0 || throw(CTBase.Exceptions.IncorrectArgument(
"Invalid max_iter", got = "$x", expected = ">= 0",
)),
)max_iter (maxiter)::Integer (default: 1000)Fields
| Field | Type | Description |
|---|---|---|
name | Symbol | Primary option name |
type | Type | Expected Julia type |
default | Any | Default value (or NotProvided) |
description | String | Human-readable description |
aliases | Tuple{Vararg{Symbol}} | Alternative names |
validator | Function or nothing | Validation function |
Constructor validation
The constructor automatically:
Checks that
defaultmatches the declaredtypeRuns the
validatoron thedefaultvalue (if both are provided)Skips validation when
defaultisNotProvided
Type mismatch in the constructor:
julia> OptionDefinition(name = :count, type = Integer, default = "hello", description = "A count")
IncorrectArgument → top-level scope, REPL[1]:2
│
│ Type mismatch in option definition
│
│ Got default value hello of type String
│ Expected value of type Integer
│
│ Context OptionDefinition constructor - validating type compatibility
│ Hint Ensure the default value matches the declared type, or adjust the type parameter
└─Aliases
Aliases allow users to use alternative names for the same option:
def_alias = OptionDefinition(
name = :max_iter, type = Int, default = 100,
description = "Max iterations", aliases = (:maxiter, :max),
)
all_names(def_alias)(:max_iter, :maxiter, :max)The extraction system searches all names when looking for a match in kwargs.
Validators
Validators follow the pattern x -> condition || throw(...). They should return a truthy value on success or throw on failure:
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 failure:
julia> extract_option((tol = -1.0,), validated_def)
IncorrectArgument → #5, options-system.md:97
│
│ Invalid tolerance
│
│ Got tol=-1.0
│ Expected positive real number (> 0)
│
│ Hint Use 1e-6 or 1e-8
└─NotProvided
NotProvided is a sentinel value that distinguishes "no default" from "default is nothing":
NotProvidedNotProvided# Option with NotProvided default — omitted if user doesn't provide it
opt_np = OptionDefinition(
name = :mu_init, type = Real, default = NotProvided,
description = "Initial barrier parameter",
)mu_init::Real (default: NotProvided)When extract_option encounters a NotProvided default and the user hasn't provided the option, the option is excluded from the result:
result, remaining = extract_option((other = 42,), opt_np)
println("Result: ", result)
println("Remaining: ", remaining)Result: NotStored
Remaining: (other = 42,)OptionValue and Provenance
OptionValue wraps a value with its provenance — where it came from:
OptionValue(1000, :user)1000 (user)OptionValue(1e-8, :default)1.0e-8 (default)OptionValue(42, :computed)42 (computed)Three sources
| Source | Meaning |
|---|---|
:user | Explicitly provided by the user |
:default | Came from the OptionDefinition default |
:computed | Derived or computed from other options |
Invalid source:
julia> OptionValue(42, :invalid_source)
IncorrectArgument → top-level scope, REPL[1]:2
│
│ Invalid option source
│
│ Got source=invalid_source
│ Expected :default, :user, or :computed
│
│ Context OptionValue constructor - validating source provenance
│ Hint Use one of the valid source symbols: :default (tool default), :user (user-provided), or :computed (derived)
└─Provenance tracking enables introspection — you can tell whether a value was explicitly chosen or inherited from defaults:
opt = OptionValue(1000, :user)
println("Value: ", opt.value)
println("Source: ", opt.source)Value: 1000
Source: userAccessing Option Properties
Use the getters in Options to access OptionDefinition and OptionValue fields instead of reading struct fields directly. This keeps encapsulation intact and aligns with Strategies overrides.
using CTBase.Options
def2 = OptionDefinition(
name = :max_iter,
type = Int,
default = 100,
description = "Maximum iterations",
aliases = (:maxiter,),
)
opt2 = OptionValue(200, :user)julia> Options.name(def2)
:max_iter
julia> Options.type(def2)
Int64
julia> Options.default(def2)
100
julia> Options.description(def2)
"Maximum iterations"
julia> Options.aliases(def2)
(:maxiter,)
julia> Options.is_required(def2)
false
julia> Options.value(opt2)
200
julia> Options.source(opt2)
:user
julia> Options.is_user(opt2)
true
julia> Options.is_default(opt2)
false
julia> Options.is_computed(opt2)
falseStrategyMetadata Overview
StrategyMetadata is a collection of OptionDefinition objects that describes all configurable options for a strategy. It is returned by Strategies.metadata(::Type).
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"),
)StrategyMetadata with 3 options:
│
├─ tol::Real (default: 1.0e-8)
│ description: Tolerance
│
├─ max_iter::Integer (default: 1000)
│ description: Max iterations
│
└─ verbose::Bool (default: false)
description: Verbose outputCollection interface
StrategyMetadata implements the standard Julia collection interface:
println("keys: ", keys(meta))
println("length: ", length(meta))
println("haskey: ", haskey(meta, :tol))keys: (:tol, :max_iter, :verbose)
length: 3
haskey: truemeta[:tol]tol::Real (default: 1.0e-8)Uniqueness
The constructor validates that all option names (including aliases) are unique across the entire metadata collection.
StrategyOptions
StrategyOptions stores the validated option values for a strategy instance. It is created by build_strategy_options.
abstract type DemoStrategy <: CTBase.Strategies.AbstractStrategy end
CTBase.Strategies.id(::Type{DemoStrategy}) = :demo
CTBase.Strategies.metadata(::Type{DemoStrategy}) = metaopts = CTBase.Strategies.build_strategy_options(DemoStrategy;
max_iter = 500, tol = 1e-6,
)StrategyOptions with 3 options:
├─ max_iter = 500 [user]
├─ tol = 1.0e-6 [user]
└─ verbose = false [default]Access patterns
println("opts[:max_iter] = ", opts[:max_iter])
println("opts[:tol] = ", opts[:tol])
println("opts[:verbose] = ", opts[:verbose])opts[:max_iter] = 500
opts[:tol] = 1.0e-6
opts[:verbose] = falseCollection interface
println("keys: ", keys(opts))
println("length: ", length(opts))
println("haskey: ", haskey(opts, :tol))keys: (:max_iter, :tol, :verbose)
length: 3
haskey: truefor (k, v) in pairs(opts)
println(" ", k, " => ", v)
end max_iter => 500
tol => 1.0e-6
verbose => falseConversion to Dict
StrategyOptions can be converted to a mutable Dict for modification before passing to backend solvers or model builders:
dict = CTBase.Strategies.options_dict(opts)
println("Type: ", typeof(dict))
println("max_iter: ", dict[:max_iter])Type: Dict{Symbol, Any}
max_iter: 500The conversion unwraps OptionValue wrappers and filters out NotProvided values:
# Modify the dict (doesn't affect original StrategyOptions)
dict[:max_iter] = 1000
println("Dict: ", dict[:max_iter])
println("Original: ", opts[:max_iter])Dict: 1000
Original: 500This pattern is commonly used in solver extensions and modelers to customize options before passing them to backend implementations.
Encapsulation Best Practices
Prefer the Options and Strategies getters over direct field access:
opts[:key]— raw option valueopts.key— fullOptionValue(value + provenance), displayed as500 (user)CTBase.Strategies.option(opts, :key)— same as dot notationOptions.value(opts, :key),Options.source(opts, :key)— scalar accessOptions.is_user(opts, :key),Options.is_default(opts, :key)— provenance predicates
Using opts defined above:
julia> CTBase.Strategies.option(opts, :max_iter)
500 (user)
julia> Options.value(opts, :max_iter)
500
julia> Options.source(opts, :max_iter)
:user
julia> Options.is_user(opts, :max_iter)
true
julia> Options.is_default(opts, :verbose)
trueDirect access shortcut on strategy instances
When working with a concrete strategy, strategy[:key] is syntactic sugar for Strategies.options(strategy)[:key] — both return the raw value. See Implementing a Strategy for a complete example.
Validation Modes
build_strategy_options supports two validation modes.
Strict mode (default)
Rejects unknown options with a helpful error message:
julia> CTBase.Strategies.build_strategy_options(DemoStrategy; max_itr = 500)
IncorrectArgument → top-level scope, REPL[1]:2
│
│ Unknown options provided for DemoStrategy
│
│ Unrecognized options: [:max_itr]
│
│ These options are not defined in the metadata of DemoStrategy.
│
│ Available options:
│ :max_iter, :tol, :verbose
│
│ Suggestions for :max_itr:
│ - :max_iter [distance: 1]
│ - :tol [distance: 7]
│ - :verbose [distance: 7]
│
│ If you are certain these options exist for the backend,
│ use permissive mode:
│ DemoStrategy(...; mode=:permissive)
│
│ Context build_strategy_options - strict validation
└─Permissive mode
Accepts unknown options with a warning and stores them with :user source:
opts_perm = CTBase.Strategies.build_strategy_options(DemoStrategy;
mode = :permissive, max_iter = 500, custom_flag = true,
)
println("keys: ", keys(opts_perm))┌ Warning: Unrecognized options passed to backend
│
│ Unvalidated options: [:custom_flag]
│
│ These options will be passed directly to the DemoStrategy backend
│ without validation by CTBase. Ensure they are correct.
└ @ CTBase.Strategies ~/work/CTBase.jl/CTBase.jl/src/Strategies/api/validation_helpers.jl:105
keys: (:max_iter, :tol, :verbose, :custom_flag)Extraction Functions
extract_option
Extracts a single option from a NamedTuple:
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)
println("Extracted: ", opt_value)
println("Remaining: ", remaining)Extracted: 200 (user)
Remaining: (tol = 1.0e-6,)The function:
Searches all names (primary + aliases)
Validates the type
Runs the validator
Returns
OptionValuewith:usersourceRemoves the matched key from remaining kwargs
Type mismatch in extraction:
julia> extract_option((grid_size = "hello",), def_grid)
IncorrectArgument → top-level scope, REPL[1]:2
│
│ Invalid option type
│
│ Got value hello of type String
│ Expected Int64
│
│ Context Option extraction for grid_size
│ Hint Ensure the option value matches the expected type
└─extract_options
Extracts multiple options at once:
defs = [
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)
println("Extracted: ", extracted)
println("Remaining: ", remaining)Extracted: Dict{Symbol, CTBase.Options.OptionValue}(:grid_size => 200 (user), :tol => 1.0e-6 (default))
Remaining: (max_iter = 1000,)extract_raw_options
Unwraps OptionValue wrappers and filters out NotProvided values:
raw_input = (
backend = OptionValue(:optimized, :user),
show_time = OptionValue(false, :default),
optional = OptionValue(NotProvided, :default),
)
extract_raw_options(raw_input)(show_time = false, backend = :optimized)Data Flow Summary
User kwargs StrategyMetadata
(max_iter=500, tol=1e-6) (OptionDefinition collection)
│ │
└──────────────┬──────────────┘
▼
build_strategy_options
(validate, merge, track provenance)
│
▼
StrategyOptions
(max_iter=500 :user, tol=1e-6 :user,
print_level=5 :default)
│
▼
options_dict
(Dict for backend)