Validation and Constraints
Custom validation hooks and optimization constraints
SimOptDecisions provides validation hooks for catching configuration errors early and constraint types for guiding optimization.
Config Validation
Override validate to add domain-specific validation for your configuration:
function SimOptDecisions.validate(config::MyConfig)
config.horizon > 0 || return false
config.discount_rate >= 0 || return false
return true
endThis is called automatically before optimization starts. If it returns false, optimization throws an ArgumentError.
Policy-Config Compatibility
Override the two-argument form to validate that a policy is compatible with a config:
function SimOptDecisions.validate(policy::MyPolicy, config::MyConfig)
# Example: policy parameter must be within config bounds
policy.threshold <= config.max_threshold || return false
return true
endOptimization Constraints
For optimization, you can add constraints that guide the search beyond simple parameter bounds.
FeasibilityConstraint
A hard constraint that must be satisfied for a solution to be considered valid:
# Policy is only feasible if total allocation <= 1.0
feasibility = FeasibilityConstraint(
:budget_limit,
policy -> sum(params(policy)) <= 1.0
)
prob = OptimizationProblem(
config, scenarios, MyPolicy, calculate_metrics, objectives;
constraints=[feasibility]
)The constraint function takes a policy and returns true if feasible, false otherwise. Infeasible solutions are rejected by the optimizer.
PenaltyConstraint
A soft constraint that adds a penalty to objectives when violated:
# Penalize solutions that exceed budget (but don't reject them)
penalty = PenaltyConstraint(
:soft_budget,
policy -> max(0.0, sum(params(policy)) - 1.0), # 0 if satisfied
1000.0 # weight
)
prob = OptimizationProblem(
config, scenarios, MyPolicy, calculate_metrics, objectives;
constraints=[penalty]
)The constraint function returns: - 0.0 when satisfied (no penalty) - A positive value proportional to the violation magnitude
The penalty is multiplied by the weight and added to all objectives.
Built-in Validation
The framework automatically validates:
Policy Interface
Before optimization, the framework checks that your policy type:
- Implements
param_bounds(::Type{MyPolicy})returningVector{Tuple} - Has a vector constructor
MyPolicy(x::AbstractVector) - Returns bounds where
lower <= upper - Has at least one parameter
# These are checked automatically:
SimOptDecisions.param_bounds(::Type{MyPolicy}) = [(0.0, 1.0), (0.0, 10.0)]
MyPolicy(x::AbstractVector) = MyPolicy(x[1], x[2])Scenario Collection
Scenarios must be: - Non-empty - All the same concrete type - Subtypes of AbstractScenario
Objectives
Objectives must be: - Non-empty (at least one objective) - Unique names (no duplicates) - Created with minimize(:name) or maximize(:name)
Parameter Type Validation
For explore(), your Scenario, Policy, and Outcome types must use parameter wrappers (ContinuousParameter, DiscreteParameter, CategoricalParameter, TimeSeriesParameter, or GenericParameter).
When Are Parameter Wrappers Required?
| Function | Parameter Types | Notes |
|---|---|---|
simulate() |
Not required | Plain types work fine |
explore() |
Required | Always validates; errors if types aren’t wrapped |
optimize() |
Recommended | Enables automatic param_bounds derivation |
Error Messages
If parameter type validation fails, you get a helpful error:
ParameterTypeError: Scenario type MyScenario has non-parameter fields:
- discount_rate :: Float64
All fields must be one of:
- ContinuousParameter{T} -- continuous real values with bounds
- DiscreteParameter{T} -- integer values with optional valid_values
- CategoricalParameter{T} -- categorical values with defined levels
- TimeSeriesParameter{T,I} -- time-indexed data
- GenericParameter{T} -- complex objects (skipped in explore/flatten)
Example fix:
# Before
struct MyScenario
discount_rate::Float64
end
# After
struct MyScenario{T<:AbstractFloat}
discount_rate::ContinuousParameter{T}
end
Example: Complete Validation Setup
using SimOptDecisions
# Custom config validation
function SimOptDecisions.validate(config::HouseConfig)
config.horizon > 0 || return false
config.house_value > 0 || return false
return true
end
# Policy-config compatibility
function SimOptDecisions.validate(policy::ElevationPolicy, config::HouseConfig)
# Elevation can't exceed physical maximum
value(policy.elevation_ft) <= 14.0 || return false
return true
end
# Set up optimization with constraints
feasibility = FeasibilityConstraint(
:minimum_elevation,
p -> value(p.elevation_ft) == 0.0 || value(p.elevation_ft) >= 3.0 # Either 0 or at least 3 ft
)
prob = OptimizationProblem(
config, scenarios, ElevationPolicy, calculate_metrics,
[minimize(:expected_cost), minimize(:worst_case)];
constraints=[feasibility]
)
# Validation happens automatically when you call optimize()
result = SimOptDecisions.optimize(prob, backend)Error Messages
When validation fails, you get descriptive error messages:
ArgumentError: Policy type MyPolicy must implement `param_bounds(::Type{MyPolicy})`
returning a Vector of (lower, upper) tuples
ArgumentError: param_bounds(::Type{MyPolicy})[2] has lower > upper: 10.0 > 5.0
ArgumentError: All scenarios must be the same concrete type.
Scenario 1 is ClimateScenario{Float64}, but Scenario 42 is ClimateScenario{Float32}
These messages tell you exactly what’s wrong and how to fix it.