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 = 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:

  1. Checks that default matches the declared type

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

  3. Skips validation when default is NotProvided

Type mismatch in the constructor:

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

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

julia
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
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":

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

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

julia
OptionValue(1000, :user)
1000 (user)
julia
OptionValue(1e-8, :default)
1.0e-8 (default)
julia
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
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:

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

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

Extraction Functions

extract_option

Extracts a single option from a NamedTuple:

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

  1. Searches all names (primary + aliases)

  2. Validates the type

  3. Runs the validator

  4. Returns OptionValue with :user source

  5. Removes the matched key from remaining kwargs

Type mismatch in extraction:

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

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

julia
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

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)