From aea02a410e7ffe08c38347f4c13725b80aae340b Mon Sep 17 00:00:00 2001 From: m-bossart Date: Fri, 25 Sep 2026 14:51:45 -0700 Subject: [PATCH] Route every start value through set_start_value! Add `set_start_value!(container, variable, value)`, which applies a start value only when warm start is enabled, and use it in the device and service variable builders. Downstream builders that create variables directly call the same function, so `warm_start = false` reaches the solver with no start values. Add a `skip_variable(V, device, F)` hook that leaves a device off a variable's axis, so downstream builders that only filtered devices can use the generic builder. --- src/InfrastructureOptimizationModels.jl | 1 + src/common_models/add_variable.jl | 41 +++++--- src/common_models/interfaces.jl | 6 ++ test/InfrastructureOptimizationModelsTests.jl | 1 + test/test_start_values.jl | 95 +++++++++++++++++++ 5 files changed, 132 insertions(+), 12 deletions(-) create mode 100644 test/test_start_values.jl diff --git a/src/InfrastructureOptimizationModels.jl b/src/InfrastructureOptimizationModels.jl index e262e19..11d4f1b 100644 --- a/src/InfrastructureOptimizationModels.jl +++ b/src/InfrastructureOptimizationModels.jl @@ -518,6 +518,7 @@ export AbstractOptimizationProblem # Settings and Data Types export Settings export get_warm_start +export set_start_value! export get_horizon, get_initial_time, get_optimizer, get_ext, get_interval export get_check_components, get_initialize_model, get_initialization_file export get_deserialize_initial_conditions, get_export_pwl_vars diff --git a/src/common_models/add_variable.jl b/src/common_models/add_variable.jl index 1eebf8f..9fa4a06 100644 --- a/src/common_models/add_variable.jl +++ b/src/common_models/add_variable.jl @@ -1,3 +1,23 @@ +""" +Set `value` as the start value of `variable` when warm start is enabled in the container +settings. A `nothing` value leaves the variable without a start value. + +Every start value in a model built on this package should go through this function, so a +model built with `warm_start = false` reaches the solver with no start values at all. Some +solvers treat a partial start as a sub-problem to complete before presolve, so a single +stray start value can cost as much as a full warm start. +""" +function set_start_value!( + container::OptimizationContainer, + variable::JuMP.VariableRef, + value::Union{Nothing, Real}, +) + value === nothing && return + get_warm_start(get_settings(container)) || return + JuMP.set_start_value(variable, value) + return +end + @doc raw""" Adds a variable to the optimization model and to the affine expressions contained in the optimization_container model according to the specified sign. Based on the inputs, the variable can @@ -43,18 +63,18 @@ function add_variables!( } where {D <: IS.InfrastructureSystemsComponent} @assert !isempty(devices) time_steps = get_time_steps(container) - settings = get_settings(container) binary = get_variable_binary(T, D, F) + included = [d for d in devices if !skip_variable(T, d, F)] variable = add_variable_container!( container, T, D, - get_name.(devices), + String[get_name(d) for d in included], time_steps, ) - for t in time_steps, d in devices + for t in time_steps, d in included name = get_name(d) variable[name, t] = JuMP.@variable( get_jump_model(container), @@ -67,10 +87,11 @@ function add_variables!( lb = get_variable_lower_bound(T, d, F) lb !== nothing && JuMP.set_lower_bound(variable[name, t], lb) - if get_warm_start(settings) - init = get_variable_warm_start_value(T, d, F) - init !== nothing && JuMP.set_start_value(variable[name, t], init) - end + set_start_value!( + container, + variable[name, t], + get_variable_warm_start_value(T, d, F), + ) end return @@ -154,7 +175,6 @@ function _add_service_device_variables!( D <: IS.InfrastructureSystemsComponent, F <: AbstractServiceFormulation, } - settings = get_settings(container) binary = get_variable_binary(T, U, F) service_name = IS.get_name(service) jump_model = get_jump_model(container) @@ -170,10 +190,7 @@ function _add_service_device_variables!( ub !== nothing && JuMP.set_upper_bound(var, ub) lb = get_variable_lower_bound(T, service, d, F) lb !== nothing && !binary && JuMP.set_lower_bound(var, lb) - if get_warm_start(settings) - init = get_variable_warm_start_value(T, d, F) - init !== nothing && JuMP.set_start_value(var, init) - end + set_start_value!(container, var, get_variable_warm_start_value(T, d, F)) end return end diff --git a/src/common_models/interfaces.jl b/src/common_models/interfaces.jl index 77d9e3e..1ccf1e7 100644 --- a/src/common_models/interfaces.jl +++ b/src/common_models/interfaces.jl @@ -76,6 +76,12 @@ get_variable_upper_bound( ::Type{<:Union{AbstractDeviceFormulation, AbstractServiceFormulation}}, ) = nothing +""" +Extension point: whether `add_variables!` creates no variable for this device. A skipped +device is left off the variable container's axis. +""" +skip_variable(::Type{<:VariableType}, ::IS.InfrastructureSystemsComponent, ::Type) = false + """ Extension point: Get variable warm start value. """ diff --git a/test/InfrastructureOptimizationModelsTests.jl b/test/InfrastructureOptimizationModelsTests.jl index bded2c1..2f63d35 100644 --- a/test/InfrastructureOptimizationModelsTests.jl +++ b/test/InfrastructureOptimizationModelsTests.jl @@ -137,6 +137,7 @@ function run_tests() # TODO tests? include(joinpath(TEST_DIR, "test_jump_utils.jl")) include(joinpath(TEST_DIR, "test_pwl_methods.jl")) + include(joinpath(TEST_DIR, "test_start_values.jl")) include(joinpath(TEST_DIR, "test_ramp_constraints.jl")) include(joinpath(TEST_DIR, "test_parameterized_range_constraints.jl")) include(joinpath(TEST_DIR, "test_duration_constraints.jl")) diff --git a/test/test_start_values.jl b/test/test_start_values.jl new file mode 100644 index 0000000..03cfa5f --- /dev/null +++ b/test/test_start_values.jl @@ -0,0 +1,95 @@ +struct WarmStartTestVariable <: IOM.VariableType end + +IOM.get_variable_binary( + ::Type{WarmStartTestVariable}, + ::Type{MockThermalGen}, + ::Type{TestDeviceFormulation}, +) = false +IOM.get_variable_warm_start_value( + ::Type{WarmStartTestVariable}, + d::MockThermalGen, + ::Type{TestDeviceFormulation}, +) = d.active_power_limits.max + +struct SkippedTestVariable <: IOM.VariableType end + +IOM.get_variable_binary( + ::Type{SkippedTestVariable}, + ::Type{MockThermalGen}, + ::Type{TestDeviceFormulation}, +) = false +IOM.skip_variable( + ::Type{SkippedTestVariable}, + d::MockThermalGen, + ::Type{TestDeviceFormulation}, +) = d.must_run + +function _warm_start_container(warm_start::Bool) + sys = MockSystem(100.0) + settings = IOM.Settings( + sys; + horizon = Dates.Hour(2), + resolution = Dates.Hour(1), + warm_start = warm_start, + ) + container = IOM.OptimizationContainer(sys, settings, JuMP.Model(), IS.Deterministic) + IOM.set_time_steps!(container, 1:2) + return container +end + +@testset "Start values" begin + @testset "set_start_value! follows the warm_start setting" begin + for warm_start in (true, false) + container = _warm_start_container(warm_start) + var = JuMP.@variable(IOM.get_jump_model(container)) + IOM.set_start_value!(container, var, 2.5) + @test JuMP.start_value(var) == (warm_start ? 2.5 : nothing) + end + end + + @testset "set_start_value! with nothing sets no start value" begin + container = _warm_start_container(true) + var = JuMP.@variable(IOM.get_jump_model(container)) + IOM.set_start_value!(container, var, nothing) + @test JuMP.start_value(var) === nothing + end + + @testset "add_variables! applies the warm start hook only with warm_start" begin + devices = [make_mock_thermal("gen1"; limits = (min = 0.0, max = 40.0))] + for warm_start in (true, false) + container = _warm_start_container(warm_start) + IOM.add_variables!( + container, + WarmStartTestVariable, + devices, + TestDeviceFormulation, + ) + var = IOM.get_variable(container, WarmStartTestVariable, MockThermalGen) + for t in 1:2 + @test JuMP.start_value(var["gen1", t]) == (warm_start ? 40.0 : nothing) + end + end + end + + @testset "add_variables! leaves skipped devices off the axis" begin + container = _warm_start_container(true) + devices = [ + make_mock_thermal("committed"), + make_mock_thermal("must_run"; must_run = true), + ] + IOM.add_variables!(container, SkippedTestVariable, devices, TestDeviceFormulation) + var = IOM.get_variable(container, SkippedTestVariable, MockThermalGen) + @test axes(var)[1] == ["committed"] + + container = _warm_start_container(true) + all_skipped = [make_mock_thermal("must_run"; must_run = true)] + IOM.add_variables!( + container, + SkippedTestVariable, + all_skipped, + TestDeviceFormulation, + ) + var = IOM.get_variable(container, SkippedTestVariable, MockThermalGen) + @test isempty(axes(var)[1]) + end +end