Skip to content

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.

julia
using CTBase

Overview ​

The options system has four core types and a set of extraction functions:

text
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.

julia
def = Options.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 ​

FieldTypeDescription
nameSymbolPrimary option name
typeTypeExpected Julia type
defaultAnyDefault value (or NotProvided)
descriptionStringHuman-readable description
aliasesTuple{Vararg{Symbol}}Alternative names
validatorFunction or nothingValidation function

Constructor validation ​

The constructor automatically:

  • Checks that default matches the declared type

  • Runs the validator on the default value (if both are provided)

  • Skips validation when default is NotProvided

Type mismatch in the constructor:

julia> Options.OptionDefinition(
           name = :count, type = Integer,
           default = "hello", description = "A count",
       )
ERROR: IncorrectArgument
│
│  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:

julia
def_alias = Options.OptionDefinition(
    name = :max_iter, type = Int, default = 100,
    description = "Max iterations",
    aliases = (:maxiter, :max),
)
Options.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:

julia
validated_def = Options.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> Options.extract_option((tol = -1.0,), validated_def)
ERROR: IncorrectArgument
│
│  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":

julia
Core.NotProvided
NotProvided
julia
opt_np = Options.OptionDefinition(
    name = :mu_init, type = Real,
    default = Core.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:

julia
result, remaining = Options.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:

julia
Options.OptionValue(1000, :user)
1000 (user)
julia
Options.OptionValue(1e-8, :default)
1.0e-8 (default)
julia
Options.OptionValue(42, :computed)
42 (computed)

Three sources ​

SourceMeaning
:userExplicitly provided by the user
:defaultCame from the OptionDefinition default
:computedDerived or computed from other options

Invalid source:

julia> Options.OptionValue(42, :invalid_source)
ERROR: IncorrectArgument
│
│  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:

julia
opt = Options.OptionValue(1000, :user)
println("Value: ", opt.value)
println("Source: ", opt.source)
Value: 1000
Source: user

Accessing 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.

julia
def2 = Options.OptionDefinition(
    name = :max_iter,
    type = Int,
    default = 100,
    description = "Maximum iterations",
    aliases = (:maxiter,),
)
opt2 = Options.OptionValue(200, :user)
julia
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)
false

StrategyMetadata Overview ​

StrategyMetadata is a collection of OptionDefinition objects that describes all configurable options for a strategy. It is returned by Strategies.metadata(::Type).

julia
meta = CTBase.Strategies.StrategyMetadata(
    Options.OptionDefinition(
        name = :tol, type = Real,
        default = 1e-8, description = "Tolerance",
    ),
    Options.OptionDefinition(
        name = :max_iter, type = Integer,
        default = 1000, description = "Max iterations",
    ),
    Options.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 output

Collection interface ​

StrategyMetadata implements the standard Julia collection interface:

julia
println("keys:   ", keys(meta))
println("length: ", length(meta))
println("haskey: ", haskey(meta, :tol))
keys:   (:tol, :max_iter, :verbose)
length: 3
haskey: true
julia
meta[: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.

julia
abstract type DemoStrategy <: CTBase.Strategies.AbstractStrategy end
CTBase.Strategies.id(::Type{DemoStrategy}) = :demo
CTBase.Strategies.metadata(::Type{DemoStrategy}) = meta
julia
opts = 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 ​

julia
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]  = false

Collection interface ​

julia
println("keys:   ", keys(opts))
println("length: ", length(opts))
println("haskey: ", haskey(opts, :tol))
keys:   (:max_iter, :tol, :verbose)
length: 3
haskey: true
julia
for (k, v) in pairs(opts)
    println("  ", k, " => ", v)
end
  max_iter => 500
  tol => 1.0e-6
  verbose => false

Conversion to Dict ​

StrategyOptions can be converted to a mutable Dict for modification before passing to backend solvers or model builders:

julia
dict = CTBase.Strategies.options_dict(opts)
println("Type: ", typeof(dict))
println("max_iter: ", dict[:max_iter])
Type: Dict{Symbol, Any}
max_iter: 500

The conversion unwraps OptionValue wrappers and filters out NotProvided values:

julia
# 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: 500

This 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 value

  • opts.key — full OptionValue (value + provenance), displayed as 500 (user)

  • CTBase.Strategies.option(opts, :key) — same as dot notation

  • Options.value(opts, :key), Options.source(opts, :key) — scalar access

  • Options.is_user(opts, :key), Options.is_default(opts, :key) — provenance predicates

Using opts defined above:

julia
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)
true

Direct 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)
ERROR: IncorrectArgument
│
│  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, you can:
│  - bypass validation for a specific option (recommended):
│      DemoStrategy(..., max_itr=bypass(value))   # or force(value)
│  - or accept all unknown options with 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:

julia
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:114
keys: (:max_iter, :tol, :verbose, :custom_flag)

Extraction Functions ​

extract_option ​

Extracts a single option from a NamedTuple:

julia
def_grid = Options.OptionDefinition(
    name = :grid_size, type = Int, default = 100,
    description = "Grid size", aliases = (:n,),
)
opt_value, remaining = Options.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 OptionValue with :user source

  • Removes the matched key from remaining kwargs

Type mismatch in extraction:

julia> Options.extract_option((grid_size = "hello",), def_grid)
ERROR: IncorrectArgument
│
│  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:

julia
defs = [
    Options.OptionDefinition(
        name = :grid_size, type = Int,
        default = 100, description = "Grid",
    ),
    Options.OptionDefinition(
        name = :tol, type = Float64,
        default = 1e-6, description = "Tol",
    ),
]
extracted, remaining = Options.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:

julia
raw_input = (
    backend   = Options.OptionValue(:optimized, :user),
    show_time = Options.OptionValue(false, :default),
    optional  = Options.OptionValue(Core.NotProvided, :default),
)
Options.extract_raw_options(raw_input)
(show_time = false, backend = :optimized)

Data Flow Summary ​

text
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)