Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions BREAKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,52 @@ and provides migration guides for users upgrading between versions.

---

## v0.4.17 (2026-05-04)

**No breaking changes.**

This release adds a new `options_dict` method for `StrategyOptions` and refactors the existing `AbstractStrategy` method to delegate to it.

### Summary - v0.4.17

- Added `options_dict(opts::StrategyOptions)` method for direct conversion from StrategyOptions to Dict
- Refactored `options_dict(strategy::AbstractStrategy)` to delegate to the new StrategyOptions method
- Added comprehensive tests for the new StrategyOptions method (conversion, type stability, filtering)
- Updated docstrings for both methods with examples
- Updated documentation guide to mention the new method
- Improved architectural coherence by placing conversion logic on the type being converted

### Migration - v0.4.17

**No action required.** All existing code continues to work without changes.

**New behavior:**

- `options_dict` can now be called directly on `StrategyOptions` without a strategy instance
- The existing `options_dict(strategy::AbstractStrategy)` method delegates to the new method internally
- Both methods produce identical results

**Example:**

```julia
# New: Direct conversion from StrategyOptions
opts = Strategies.build_strategy_options(MyStrategy; max_iter=500, tol=1e-6)
dict = Strategies.options_dict(opts) # Works directly on StrategyOptions

# Existing: Conversion via strategy (still works)
strategy = MyStrategy(opts)
dict = Strategies.options_dict(strategy) # Delegates to StrategyOptions method
```

**Benefits:**

- **Better testability**: Direct conversion can be tested without creating full strategy instances
- **Reduced duplication**: Conversion logic is centralized in one place (StrategyOptions)
- **Architectural coherence**: Conversion logic belongs to the type being converted
- **No API changes**: All existing code continues to work unchanged

---

## v0.4.15 (2026-04-14)

**No breaking changes.**
Expand Down
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

---

## [0.4.17] - 2026-05-04

### Added

- **StrategyOptions to Dict conversion** - New `options_dict(opts::StrategyOptions)` method
- Converts StrategyOptions directly to mutable Dict without requiring a strategy instance
- Unwraps OptionValue wrappers and filters out NotProvided values
- Preserves explicit nothing values
- Enables direct conversion in code paths that have StrategyOptions but not a full strategy

### Changed

- **options_dict refactoring** - Refactored `options_dict(strategy::AbstractStrategy)` to delegate to new StrategyOptions method
- Reduces code duplication by centralizing conversion logic in StrategyOptions
- Improves architectural coherence (conversion logic belongs to the type being converted)
- Maintains backward compatibility (no API changes)

### Tests

- **StrategyOptions options_dict tests** - Added 4 new testsets in `test_utilities.jl`:
- Direct StrategyOptions conversion with mutable Dict verification
- Type stability check with `@inferred`
- NotProvided value filtering
- Explicit nothing value preservation

### Documentation

- **options_system.md** - Added "Conversion to Dict" subsection in StrategyOptions section
- Shows how to convert StrategyOptions to Dict directly
- Demonstrates mutability and independence from original StrategyOptions
- References the utility pattern for solver extensions

---

## [0.4.15] - 2026-04-14

### Added
Expand Down
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "CTSolvers"
uuid = "d3e8d392-8e4b-4d9b-8e92-d7d4e3650ef6"
version = "0.4.16"
version = "0.4.17"
authors = ["Olivier Cots <olivier.cots@toulouse-inp.fr>"]

[deps]
Expand Down
21 changes: 21 additions & 0 deletions docs/src/guides/options_system.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,27 @@ for (k, v) in pairs(opts)
end
```

### Conversion to Dict

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

```@example options
dict = CTSolvers.Strategies.options_dict(opts)
println("Type: ", typeof(dict))
println("max_iter: ", dict[:max_iter])
```

The conversion unwraps `OptionValue` wrappers and filters out `NotProvided` values:

```@example options
# Modify the dict (doesn't affect original StrategyOptions)
dict[:max_iter] = 1000
println("Dict: ", dict[:max_iter])
println("Original: ", opts[:max_iter])
```

This pattern is commonly used in solver extensions and modelers to customize options before passing them to backend implementations.

## Validation Modes

`build_strategy_options` supports two validation modes.
Expand Down
17 changes: 7 additions & 10 deletions src/Strategies/api/utilities.jl
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,9 @@ $(TYPEDSIGNATURES)

Extract strategy options as a mutable Dict, ready for modification.

This is a convenience method that combines three steps into one:
This is a convenience method that combines two steps into one:
1. Getting `StrategyOptions` from the strategy
2. Extracting raw values (unwrapping `OptionValue`)
3. Converting to `Dict` for modification
2. Converting to `Dict` via `options_dict(StrategyOptions)`

# Arguments
- `strategy::AbstractStrategy`: Strategy instance (solver, modeler, etc.)
Expand All @@ -91,16 +90,14 @@ julia> solve_with_ipopt(nlp; options...)
```

# Notes
This function is particularly useful in solver extensions and modelers where
you need to extract options and potentially modify them before passing to
backend solvers or model builders.
This function delegates to `options_dict(StrategyOptions)` for the actual conversion.
It is particularly useful in solver extensions and modelers where you need to extract
options and potentially modify them before passing to backend solvers or model builders.

See also: `options`, `Options.extract_raw_options`
See also: `options`, `options_dict(::StrategyOptions)`
"""
function options_dict(strategy::AbstractStrategy)
opts = options(strategy)
raw_opts = Options.extract_raw_options(_raw_options(opts))
return Dict{Symbol,Any}(pairs(raw_opts))
return options_dict(options(strategy))
end

"""
Expand Down
49 changes: 49 additions & 0 deletions src/Strategies/contract/strategy_options.jl
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,55 @@ function Base.haskey(opts::StrategyOptions, key::Symbol)
haskey(_raw_options(opts), _resolve_key(opts, key))
end

# ============================================================================
# Conversion utilities
# ============================================================================

"""
$(TYPEDSIGNATURES)

Extract strategy options as a mutable Dict, ready for modification.

This method converts StrategyOptions to a Dict by unwrapping OptionValue
wrappers and filtering out NotProvided values. The resulting Dict is mutable
and can be modified before passing to backend solvers or model builders.

# Arguments
- `opts::StrategyOptions`: Strategy options to convert

# Returns
- `Dict{Symbol, Any}`: Mutable dictionary of option values

# Example
```julia-repl
julia> using CTSolvers.Strategies, CTSolvers.Options

julia> opts = StrategyOptions(
max_iter = OptionValue(500, :user),
tolerance = OptionValue(1e-8, :default)
)

julia> dict = options_dict(opts)
Dict{Symbol, Any} with 2 entries:
:max_iter => 500
:tolerance => 1.0e-8

julia> dict[:verbose] = true # Modify as needed
true
```

# Notes
- NotProvided values are filtered out
- Explicit nothing values are preserved
- The returned Dict is mutable and independent from the original StrategyOptions

See also: `Options.extract_raw_options`, `_raw_options`
"""
function options_dict(opts::StrategyOptions)
raw_opts = Options.extract_raw_options(_raw_options(opts))
return Dict{Symbol,Any}(pairs(raw_opts))
end

# ============================================================================
# Display
# ============================================================================
Expand Down
3 changes: 2 additions & 1 deletion test/suite/strategies/test_strategy_options.jl
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,8 @@ function test_strategy_options()
# Test that getproperty does NOT resolve aliases (only canonical names)
Test.@test opts.max_iter isa Options.OptionValue
Test.@test opts.max_iter.value == 200
Test.@test_throws ErrorException opts.maxiter # Alias not supported in dot notation
# Dot notation doesn't resolve aliases - accessing via alias throws
Test.@test_throws Exception opts.maxiter

# Test source access with aliases
Test.@test Options.source(opts, :max_iter) == :user
Expand Down
75 changes: 75 additions & 0 deletions test/suite/strategies/test_utilities.jl
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,81 @@ function test_utilities()
Test.@test haskey(options, :tolerance)
end

# ====================================================================
# options_dict - StrategyOptions method
# ====================================================================

Test.@testset "options_dict - StrategyOptions method" begin
Test.@testset "Direct StrategyOptions conversion" begin
# Create StrategyOptions directly
opts = Strategies.StrategyOptions(
max_iter = Options.OptionValue(500, :user),
tolerance = Options.OptionValue(1e-8, :user),
verbose = Options.OptionValue(true, :default),
)

# Convert to Dict
dict = Strategies.options_dict(opts)

# Verify it's a Dict
Test.@test dict isa Dict{Symbol,Any}

# Verify all options are present
Test.@test haskey(dict, :max_iter)
Test.@test haskey(dict, :tolerance)
Test.@test haskey(dict, :verbose)

# Verify values are correct (unwrapped from OptionValue)
Test.@test dict[:max_iter] == 500
Test.@test dict[:tolerance] == 1e-8
Test.@test dict[:verbose] == true

# Verify it's mutable
dict[:max_iter] = 1000
Test.@test dict[:max_iter] == 1000
end

Test.@testset "Type Stability" begin
opts = Strategies.StrategyOptions(
max_iter = Options.OptionValue(500, :user),
tolerance = Options.OptionValue(1e-8, :user),
)
result = Test.@inferred Strategies.options_dict(opts)
Test.@test result isa Dict{Symbol,Any}
end

Test.@testset "NotProvided filtering" begin
# Create StrategyOptions with NotProvided value
opts = Strategies.StrategyOptions(
max_iter = Options.OptionValue(500, :user),
optional = Options.OptionValue(Options.NotProvided, :default),
)

# Convert to Dict
dict = Strategies.options_dict(opts)

# Verify NotProvided is filtered out
Test.@test haskey(dict, :max_iter)
Test.@test !haskey(dict, :optional)
end

Test.@testset "Nothing preservation" begin
# Create StrategyOptions with explicit nothing
opts = Strategies.StrategyOptions(
max_iter = Options.OptionValue(500, :user),
optional = Options.OptionValue(nothing, :default),
)

# Convert to Dict
dict = Strategies.options_dict(opts)

# Verify nothing is preserved
Test.@test haskey(dict, :max_iter)
Test.@test haskey(dict, :optional)
Test.@test dict[:optional] === nothing
end
end

# ====================================================================
# Integration: Utilities pipeline
# ====================================================================
Expand Down
Loading