diff --git a/src/benders.jl b/src/benders.jl index 8dd5799a21..e1c7e475e9 100644 --- a/src/benders.jl +++ b/src/benders.jl @@ -189,7 +189,9 @@ function _init_benders_invested_available!(m_mp, m) end function process_master_problem_solution(m_mp, m) - _do_save_outputs!(m_mp, (:units_invested_available, :connections_invested_available, :storages_invested_available)) + _do_save_outputs!( + m_mp, (:units_invested_available, :connections_invested_available, :storages_invested_available), (;) + ) _update_benders_invested_available!(m_mp) end @@ -206,6 +208,8 @@ function _update_benders_invested_available!(m_mp) end function process_subproblem_solution(m, k) + win_weight = window_weight(model=m.ext[:spineopt].instance, i=k, _strict=false) + win_weight = win_weight !== nothing ? win_weight : 1.0 _wait_for_dual_solves(m) _do_save_outputs!( m, @@ -214,81 +218,27 @@ function process_subproblem_solution(m, k) :bound_connections_invested_available, :bound_storages_invested_available, :unit_flow, - ) + ), + (;); + weight=win_weight, ) if k == 1 m.ext[:spineopt].extras[:sp_objective_value_bi] = 0 end - m.ext[:spineopt].extras[:sp_objective_value_bi] += sum(values(m.ext[:spineopt].values[:total_costs])) + m.ext[:spineopt].extras[:sp_objective_value_bi] += win_weight * sum(values(m.ext[:spineopt].values[:total_costs])) if _is_last_window(m, k) - m.ext[:spineopt].extras[:sp_objective_value_bi] += sum(values(m.ext[:spineopt].values[:total_costs_tail])) + m.ext[:spineopt].extras[:sp_objective_value_bi] += ( + win_weight * sum(values(m.ext[:spineopt].values[:total_costs_tail])) + ) end end -""" - _pval_by_entity(vals) - -Take the given Dict, which should be a mapping from variable indices to their value, -and return another Dict mapping entities to `ParameterValue`s. - -The keys in the result are the keys of the input, without the stochastic_scenario and the t (i.e., just the entity). -The values are `ParameterValue{Map}`s mapping the `stochastic_scenario` of the variable key, -to a `TimeSeries` mapping the `t` of the key, to the 'realized' variable value. -""" -function _pval_by_entity(vals, t_end=nothing) - by_ent = Dict() - for (ind, val) in vals - ent = _drop_key(ind, :stochastic_scenario, :t) - by_s = get!(by_ent, ent, Dict()) - by_t = get!(by_s, ind.stochastic_scenario, Dict()) - realized_val = realize(val) - if t_end !== nothing && t_end < end_(ind.t) - realized_val *= (t_end - start(ind.t)) / (end_(ind.t) - start(ind.t)) - end - by_t[ind.t] = realized_val - end - Dict( - ent => parameter_value(Map(collect(keys(by_s)), [_window_time_series(by_t) for by_t in values(by_s)])) - for (ent, by_s) in by_ent - ) -end - -""" - _window_time_series(by_t) - -A `TimeSeries` from the given `Dict` mapping `TimeSlice` to `Float64`, with an explicit NaN at the end. -The NaN is there because we want to merge marginal values from different windows of the Benders subproblem -into one `TimeSeries`. - -Without the NaN, the last value of one window would apply until the next window, which wouldn't be correct -if there were gaps between the windows (as in rolling representative periods Benders). -With the NaN, the gap is correctly skipped in the Benders cuts. -""" -function _window_time_series(by_t) - time_slices, vals = collect(keys(by_t)), collect(values(by_t)) - inds = start.(time_slices) - push!(inds, maximum(end_.(time_slices))) - push!(vals, NaN) - TimeSeries(inds, vals) -end - function _is_last_window(m, k) k == m.ext[:spineopt].temporal_structure[:window_count] end -function _save_sp_unit_flow(m) - window_values = Dict( - k => v for (k, v) in m.ext[:spineopt].values[:unit_flow] if iscontained(k.t, current_window(m)) - ) - pval_by_ent = _pval_by_entity(window_values) - pvals_to_node = Dict( - ent => Dict(:sp_unit_flow => pval) for (ent, pval) in pval_by_ent if ent.direction == direction(:to_node) - ) - pvals_from_node = Dict( - ent => Dict(:sp_unit_flow => pval) for (ent, pval) in pval_by_ent if ent.direction == direction(:from_node) - ) - add_relationship_parameter_values!(unit__to_node, pvals_to_node; merge_values=true) - add_relationship_parameter_values!(unit__from_node, pvals_from_node; merge_values=true) +function _val_by_ent(m, var_name) + _output_value_by_entity(m.ext[:spineopt].outputs[var_name], model_end(model=m.ext[:spineopt].instance)) end function save_mp_objective_bounds_and_gap!(m_mp, m) @@ -325,4 +275,3 @@ function _collect_outputs!( save_outputs=true, ) end - diff --git a/src/constraints/constraint_mp_any_invested_cuts.jl b/src/constraints/constraint_mp_any_invested_cuts.jl index 1ba4672d99..edba066909 100644 --- a/src/constraints/constraint_mp_any_invested_cuts.jl +++ b/src/constraints/constraint_mp_any_invested_cuts.jl @@ -81,10 +81,6 @@ function add_constraint_mp_any_invested_cuts!(m_mp, m) ) end -function _val_by_ent(m, var_name) - _output_value_by_entity(m.ext[:spineopt].outputs[var_name], model_end(model=m.ext[:spineopt].instance)) -end - function window_sum(ts::TimeSeries, window; init=0) sum(v for (t, v) in ts if iscontained(t, window) && !isnan(v); init=init) end diff --git a/src/constraints/constraint_mp_min_res_gen_to_demand_ratio_cuts.jl b/src/constraints/constraint_mp_min_res_gen_to_demand_ratio_cuts.jl index 2da7a63eed..be9cdc1af1 100644 --- a/src/constraints/constraint_mp_min_res_gen_to_demand_ratio_cuts.jl +++ b/src/constraints/constraint_mp_min_res_gen_to_demand_ratio_cuts.jl @@ -28,13 +28,17 @@ sum ( """ function add_constraint_mp_min_res_gen_to_demand_ratio_cuts!(m_mp, m) @fetch units_invested_available, mp_min_res_gen_to_demand_ratio_slack = m_mp.ext[:spineopt].variables + benders_units_invested_available = m_mp.ext[:spineopt].downstream_outputs[:units_invested_available] + sp_unit_flow = _val_by_ent(m, :unit_flow) merge!( get!(m_mp.ext[:spineopt].constraints, :mp_min_res_gen_to_demand_ratio_cuts, Dict()), Dict( (benders_iteration=bi, commodity=comm) => @constraint( m_mp, + sum( - window_sum_duration(m_mp, sp_unit_flow(unit=u, node=n, direction=d, stochastic_scenario=s), window) + window_sum_duration( + m_mp, sp_unit_flow[(unit=u, node=n, direction=d, stochastic_scenario=s)], window + ) for window in m_mp.ext[:spineopt].temporal_structure[:sp_windows] for (u, s) in _unit_scenario(unit(is_renewable=true)) for (u, n, d) in unit__to_node(unit=u, node=node__commodity(commodity=comm), _compact=false); @@ -43,7 +47,7 @@ function add_constraint_mp_min_res_gen_to_demand_ratio_cuts!(m_mp, m) + sum( sum( + units_invested_available[u, s, t] - - internal_fix_units_invested_available(unit=u, stochastic_scenario=s, t=t, _default=0) + - benders_units_invested_available[(unit=u, stochastic_scenario=s)](t=t) for (u, s, t) in units_invested_available_indices( m_mp; unit=u, stochastic_scenario=s, t=to_time_slice(m_mp; t=window) ); @@ -106,4 +110,13 @@ function _unit_scenario(unit) for (u, ss) in units_on__stochastic_structure(unit=unit, _compact=false) for s in stochastic_structure__stochastic_scenario(stochastic_structure=ss) ) -end \ No newline at end of file +end + +function window_sum_duration(m, ts::TimeSeries, window; init=0) + dur_unit = _model_duration_unit(m.ext[:spineopt].instance) + time_slice_value_iter = ( + (TimeSlice(t1, t2; duration_unit=dur_unit), v) for (t1, t2, v) in zip(ts.indexes, ts.indexes[2:end], ts.values) + ) + sum(v * duration(t) for (t, v) in time_slice_value_iter if iscontained(start(t), window) && !isnan(v); init=init) +end +window_sum_duration(m, x::Number, window; init=0) = x * duration(window) + init \ No newline at end of file diff --git a/src/run_spineopt_basic.jl b/src/run_spineopt_basic.jl index 5ce4b59ee2..2ebb303f23 100644 --- a/src/run_spineopt_basic.jl +++ b/src/run_spineopt_basic.jl @@ -849,7 +849,7 @@ function _save_outputs!(m, output_suffix) _do_save_outputs!(m, _output_names(m), output_suffix) end -function _do_save_outputs!(m, output_names, output_suffix=(;)) +function _do_save_outputs!(m, output_names, output_suffix; weight=1) w_start, w_end = start(current_window(m)), end_(current_window(m)) for out_name in output_names value = get(m.ext[:spineopt].values, out_name, nothing) @@ -861,7 +861,7 @@ function _do_save_outputs!(m, output_names, output_suffix=(;)) by_suffix = get!(m.ext[:spineopt].outputs, out_name, Dict()) by_window = get!(by_suffix, output_suffix, Dict()) by_window[w_start, w_end] = Dict( - _static(ind) => val for (ind, val) in _output_value_by_ind(m, something(value, param)) + _static(ind) => weight * val for (ind, val) in _output_value_by_ind(m, something(value, param)) ) end end diff --git a/src/util/misc.jl b/src/util/misc.jl index b24c27ab2c..c69b938b0d 100644 --- a/src/util/misc.jl +++ b/src/util/misc.jl @@ -163,15 +163,6 @@ function print_solution(m, variable_patterns...) println() end -function window_sum_duration(m, ts::TimeSeries, window; init=0) - dur_unit = _model_duration_unit(m.ext[:spineopt].instance) - time_slice_value_iter = ( - (TimeSlice(t1, t2; duration_unit=dur_unit), v) for (t1, t2, v) in zip(ts.indexes, ts.indexes[2:end], ts.values) - ) - sum(v * duration(t) for (t, v) in time_slice_value_iter if iscontained(start(t), window) && !isnan(v); init=init) -end -window_sum_duration(m, x::Number, window; init=0) = x * duration(window) + init - """ align_variable_duration_unit(_duration::Union{Period, Nothing}, dt::DateTime; ahead::Bool=true) diff --git a/src/util/promise.jl b/src/util/promise.jl index 2d41aa0d4c..e75d8ce34e 100644 --- a/src/util/promise.jl +++ b/src/util/promise.jl @@ -29,7 +29,9 @@ struct ReducedCostPromise <: AbstractPromise value::JuMP.VariableRef end -realize(x::DualPromise) = has_duals(owner_model(x.value)) ? dual(x.value) : 0.0 -realize(x::ReducedCostPromise) = has_duals(owner_model(x.value)) ? reduced_cost(x.value) : 0.0 +realize(x::DualPromise, upd=nothing) = has_duals(owner_model(x.value)) ? dual(x.value) : 0.0 +realize(x::ReducedCostPromise, upd=nothing) = has_duals(owner_model(x.value)) ? reduced_cost(x.value) : 0.0 -Base.:+(x::X, y::Y) where {X<:AbstractPromise,Y<:AbstractPromise} = Call(+, x, y) +Base.:*(x, y::AbstractPromise) = Call(*, x, y) + +Base.:+(x::AbstractPromise, y::AbstractPromise) = Call(+, x, y)