diff --git a/CHANGELOG.md b/CHANGELOG.md index d5c1616f7e..7bff35c294 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Fixed + +- Stop myopic multistage runs immediately when a stage has no primal solution, +reporting the solver statuses instead of failing in a later stage (#918). + ## [0.4.6] - 2026-01-06 ### Fixed diff --git a/src/multi_stage/myopic.jl b/src/multi_stage/myopic.jl index d39bb5a909..924a7c2d26 100644 --- a/src/multi_stage/myopic.jl +++ b/src/multi_stage/myopic.jl @@ -17,6 +17,17 @@ returns: * models\_d – Dictionary which contains a JuMP model for each model stage, modified by this method. * inputs\_d – Dictionary of inputs for each model stage, generated by the load\_inputs() method, modified by this method. """ +function _assert_myopic_stage_has_solution(model::Model, stage::Integer) + has_values(model) && return nothing + + error( + "Myopic multistage stage $stage has no primal solution " * + "(termination_status=$(termination_status(model)), " * + "primal_status=$(primal_status(model)), raw_status=$(raw_status(model))). " * + "Aborting before subsequent stages.", + ) +end + function run_myopic_multistage(outpath::AbstractString, models_d::Dict, setup::Dict, inputs_d::Dict) settings_d = setup["MultiStageSettingsDict"] @assert settings_d["Myopic"] == 1 "run_myopic_multistage is only valid for myopic models. update your settings to have Myopic: 1." @@ -49,6 +60,7 @@ function run_myopic_multistage(outpath::AbstractString, models_d::Dict, setup::D # Solve the model at time t @objective(models_d[t], Min, models_d[t][:eObj]) models_d[t], inputs_d[t]["solve_time"] = solve_model(models_d[t], setup) + _assert_myopic_stage_has_solution(models_d[t], t) if write_intermittent_outputs outpath_cur = joinpath(outpath, "results_p$t") @@ -57,5 +69,4 @@ function run_myopic_multistage(outpath::AbstractString, models_d::Dict, setup::D end return models_d, inputs_d - -end \ No newline at end of file +end diff --git a/test/test_multistage.jl b/test/test_multistage.jl index eba9201a1e..de2342f42f 100644 --- a/test/test_multistage.jl +++ b/test/test_multistage.jl @@ -257,4 +257,55 @@ with_logger(ConsoleLogger(stderr, Logging.Error)) do test_can_retire_validation() end +function test_myopic_stage_solution_guard() + @testset "Myopic run stops at a stage without a solution" begin + stage_1 = Model(HiGHS.Optimizer) + set_silent(stage_1) + @variable(stage_1, x) + @constraint(stage_1, x >= 1) + @constraint(stage_1, x <= 0) + @expression(stage_1, eObj, x) + + models = Dict(1 => stage_1, 2 => Model(HiGHS.Optimizer)) + setup = Dict( + "ComputeConflicts" => 0, + "NetworkExpansion" => 0, + "MultiStageSettingsDict" => Dict( + "Myopic" => 1, + "NumStages" => 2, + "WriteIntermittentOutputs" => 0 + ) + ) + stage_1_inputs = Dict( + "STOR_ALL" => Int[], + "STOR_ASYMMETRIC" => Int[], + "VRE_STOR" => Int[], + "Z" => 1 + ) + inputs = Dict(1 => stage_1_inputs, 2 => Dict{String, Any}()) + + caught_error = try + with_logger(ConsoleLogger(stderr, Logging.Error)) do + redirect_stdout(devnull) do + run_myopic_multistage("", models, setup, inputs) + end + end + nothing + catch error + error + end + + @test caught_error isa ErrorException + if caught_error isa ErrorException + message = sprint(showerror, caught_error) + @test occursin("Myopic multistage stage 1 has no primal solution", message) + @test occursin("termination_status=INFEASIBLE", message) + @test occursin("primal_status=NO_SOLUTION", message) + @test occursin("raw_status=", message) + end + end +end + +test_myopic_stage_solution_guard() + end # module TestMultiStage