From 982733bf79688e2456f2f9b0851b3dc105915301 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Str=C3=B6mer?= Date: Sat, 22 Feb 2025 00:23:43 +0100 Subject: [PATCH 01/22] feat: allow parametric expressions in profile value --- src/core/profile.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/core/profile.jl b/src/core/profile.jl index 50f46807..7cc61922 100644 --- a/src/core/profile.jl +++ b/src/core/profile.jl @@ -230,13 +230,13 @@ function _after_construct_variables!(profile::Profile) for t in get_T(model) _repr_t = internal(model).model.snapshots[t].is_representative ? t : - internal(model).model.snapshots[t].representative - val = access(profile.value, _repr_t, Float64) + internal(model).model.snapshots[t].representative - if (profile.mode === :fixed) && false # TODO _iesopt_config(model).parametric - JuMP.fix(profile.var.aux_value[t], val; force=true) - JuMP.add_to_expression!(profile.exp.value[t], profile.var.aux_value[t]) + if _isparametric(profile.value) + val = access(profile.value, _repr_t)::JuMP.VariableRef + JuMP.add_to_expression!(profile.exp.value[t], val) else + val = access(profile.value, _repr_t)::Float64 JuMP.add_to_expression!(profile.exp.value[t], val) end end From b0ecfafb157e1cd5017a30c6cfb296a7d63b9385 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Str=C3=B6mer?= Date: Sat, 22 Feb 2025 00:24:08 +0100 Subject: [PATCH 02/22] feat: allow parametric expressions in profile value --- src/core/profile/var_aux_value.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/profile/var_aux_value.jl b/src/core/profile/var_aux_value.jl index 821de6cf..a43d9828 100644 --- a/src/core/profile/var_aux_value.jl +++ b/src/core/profile/var_aux_value.jl @@ -27,6 +27,8 @@ function _profile_var_aux_value!(profile::Profile) if profile.mode === :fixed # This Profile's value is already added to the value expression. Nothing to do here. + elseif _isparametric(profile.value) + # The variable is the Parameter, nothing to do here. else # Create the variable. if !_has_representative_snapshots(model) From d44519416dfd655010470355861c3620b1f8dff8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Str=C3=B6mer?= Date: Sat, 22 Feb 2025 00:24:31 +0100 Subject: [PATCH 03/22] feat: basic implementation of parametric expressions --- src/core/expression.jl | 47 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/src/core/expression.jl b/src/core/expression.jl index e310c1f8..0989afbe 100644 --- a/src/core/expression.jl +++ b/src/core/expression.jl @@ -225,8 +225,9 @@ If the value of `my_exp` is a vector of `Float64`, the first call will succeed, dirty::Bool = false temporal::Bool = false empty::Bool = false + parametric::Bool = false - value::Union{Nothing, JuMP.VariableRef, JuMP.AffExpr, Vector{JuMP.AffExpr}, Float64, Vector{Float64}} = nothing + value::Union{Nothing, JuMP.VariableRef, Vector{JuMP.VariableRef}, JuMP.AffExpr, Vector{JuMP.AffExpr}, Float64, Vector{Float64}} = nothing internal::Union{Nothing, NamedTuple} = nothing end @@ -263,6 +264,34 @@ _isfixed(e::Expression) = ( (!any(occursin(':', el) for el in e.internal.elements)) )::Bool _isempty(e::Expression) = e.empty::Bool +_isparametric(e::Expression) = e.parametric::Bool + +function set_unknown(e::Expression, value::Real) + if !e.parametric + @critical "Only parametric expressions support `set_unknown`" + end + + if e.temporal + JuMP.set_parameter_value.(e.value, convert.(Float64, value)) + else + JuMP.set_parameter_value(e.value, convert(Float64, value)) + end + + return nothing +end + +function set_unknown(e::Expression, value::Vector{<: Real}) + if !e.parametric + @critical "Only parametric expressions support `set_unknown`" + end + + if !e.temporal + @critical "Only temporal expressions support `set_unknown` with a vector-valued argument, use `\$(t)` instead of `\$()`" + end + + JuMP.set_parameter_value.(e.value, convert.(Float64, value)) + return nothing +end @recompile_invalidations begin function Base.show(io::IO, e::Expression) @@ -293,6 +322,20 @@ macro _default_expression(value) end function _convert_to_expression(model::JuMP.Model, @nospecialize(data::AbstractString)) + if startswith(data, "\$") + # TODO: base_name = make_base_name(profile, "aux_value") + + if data == "\$()" + value = @variable(model, set = JuMP.Parameter(0.0)) + return Expression(; model, value, parametric=true) + elseif data == "\$(t)" + value = @variable(model, [t = get_T(model)], set = JuMP.Parameter(0.0), container = Array) + return Expression(; model, value, parametric=true, temporal=true) + else + @critical "Invalid expression string trying to create an unknown, either use `\$()` or `\$(t)`" data + end + end + parsed = _parse_expression(model, data, _GeneralExpressionType()) if hasproperty(parsed, :val) @@ -372,6 +415,8 @@ function access(e::Expression, t::_ID) return (e.value::Vector{JuMP.AffExpr})[t]::JuMP.AffExpr elseif e.value isa Vector{Float64} return (e.value::Vector{Float64})[t]::Float64 + elseif e.value isa Vector{JuMP.VariableRef} + (e.value::Vector{JuMP.VariableRef})[t]::JuMP.VariableRef else return e.value::Union{Nothing, JuMP.VariableRef, JuMP.AffExpr, Float64} end From 0f048e86ec71b1c9b8d740cf42eff19301e011d4 Mon Sep 17 00:00:00 2001 From: sstroemer Date: Sat, 22 Feb 2025 00:28:10 +0100 Subject: [PATCH 04/22] fix: indicate modification of expression in `set_unknown` --- src/core/expression.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/expression.jl b/src/core/expression.jl index 0989afbe..86eee0eb 100644 --- a/src/core/expression.jl +++ b/src/core/expression.jl @@ -266,7 +266,7 @@ _isfixed(e::Expression) = ( _isempty(e::Expression) = e.empty::Bool _isparametric(e::Expression) = e.parametric::Bool -function set_unknown(e::Expression, value::Real) +function set_unknown!(e::Expression, value::Real) if !e.parametric @critical "Only parametric expressions support `set_unknown`" end @@ -280,7 +280,7 @@ function set_unknown(e::Expression, value::Real) return nothing end -function set_unknown(e::Expression, value::Vector{<: Real}) +function set_unknown!(e::Expression, value::Vector{<: Real}) if !e.parametric @critical "Only parametric expressions support `set_unknown`" end From f292a2df130423ebb26a3fa8ec80b56736684dcb Mon Sep 17 00:00:00 2001 From: sstroemer Date: Sat, 22 Feb 2025 02:31:47 +0100 Subject: [PATCH 05/22] chore: add internal helper to check if a model is parametric --- src/config/config.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/config/config.jl b/src/config/config.jl index aad384af..4abda6e7 100644 --- a/src/config/config.jl +++ b/src/config/config.jl @@ -80,3 +80,4 @@ _has_representative_snapshots(model::JuMP.Model) = false # TODO _is_multiobjective(model::JuMP.Model) = (:mo in @config(model, optimization.problem_type))::Bool _is_lp(model::JuMP.Model) = (:lp in @config(model, optimization.problem_type))::Bool _is_milp(model::JuMP.Model) = (:milp in @config(model, optimization.problem_type))::Bool +_is_parametric(model::JuMP.Model) = (:parametric in @config(model, optimization.problem_type))::Bool From 7502b9a0d4ab1e86ddfbce3c8e073516e15ee5f2 Mon Sep 17 00:00:00 2001 From: sstroemer Date: Sat, 22 Feb 2025 02:33:01 +0100 Subject: [PATCH 06/22] feat: adjust internals to allow parametric expressions (tested for everything except Units) --- src/IESopt.jl | 44 +++++++++++++++++++++++-- src/core/connection/obj_cost.jl | 4 +-- src/core/decision.jl | 11 ++++--- src/core/decision/con_value_bounds.jl | 24 ++++++++++++++ src/core/decision/obj_fixed.jl | 2 +- src/core/decision/obj_value.jl | 2 +- src/core/decision/var_value.jl | 11 ------- src/core/expression.jl | 46 ++++++++++++++++++++++----- src/core/profile/obj_cost.jl | 8 ++--- src/parser.jl | 14 +++----- src/utils/general.jl | 2 +- 11 files changed, 122 insertions(+), 46 deletions(-) create mode 100644 src/core/decision/con_value_bounds.jl diff --git a/src/IESopt.jl b/src/IESopt.jl index dbd2d492..a69b3b1c 100644 --- a/src/IESopt.jl +++ b/src/IESopt.jl @@ -133,9 +133,17 @@ function _build_model!(model::JuMP.Model) end if !_is_multiobjective(model) - current_objective = @config(model, optimization.objective.current) - isnothing(current_objective) && @critical "[build] Missing an active objective" - @objective(model, Min, internal(model).model.objectives[current_objective].expr) + if !_is_parametric(model) + current_objective = @config(model, optimization.objective.current) + isnothing(current_objective) && @critical "[build] Missing an active objective" + @objective(model, Min, internal(model).model.objectives[current_objective].expr) + else + current_objective = @config(model, optimization.objective.current) + isnothing(current_objective) && @critical "[build] Missing an active objective" + + # Set an optimizer hook that helps resolving quadratic objectives for parametric models. + JuMP.set_optimize_hook(model, _optimize_hook_parametric) + end else @objective( model, @@ -143,6 +151,34 @@ function _build_model!(model::JuMP.Model) [internal(model).model.objectives[obj].expr for obj in @config(model, optimization.multiobjective.terms)] ) end + + return nothing +end + +function _optimize_hook_parametric(model::JuMP.Model) + obj = internal(model).model.objectives[@config(model, optimization.objective.current)].expr + + if obj isa JuMP.QuadExpr + @debug "[optimize > hook] Resolving quadratic objective" + obj_aff_aux = zero(JuMP.AffExpr) + + # Add the affine part of the objective. + JuMP.add_to_expression!(obj_aff_aux, obj.aff) + + # Add the quadratic part of the objective. + for (term, coeff) in obj.terms + if JuMP.is_parameter(term.a) + JuMP.add_to_expression!(obj_aff_aux, term.b, JuMP.parameter_value(term.a) * coeff) + else + JuMP.add_to_expression!(obj_aff_aux, term.a, JuMP.parameter_value(term.b) * coeff) + end + end + + # Overwrite the objective. + @objective(model, Min, obj_aff_aux) + end + + return JuMP.optimize!(model; ignore_optimize_hook=true) end function _prepare_model!(model::JuMP.Model) @@ -739,6 +775,8 @@ function _optimize!(model::JuMP.Model; @nospecialize(kwargs...)) @info "[optimize] Passing model to solver" end + # TODO: if the objective expression is still "Quad" due to a parametric model, we could substitute it with a affine expression (maybe in the hook?) + JuMP.optimize!(model; kwargs...) # todo: make use of `is_solved_and_feasible`? if, make sure the version requirement of JuMP is correct diff --git a/src/core/connection/obj_cost.jl b/src/core/connection/obj_cost.jl index 38e153a6..b49c5870 100644 --- a/src/core/connection/obj_cost.jl +++ b/src/core/connection/obj_cost.jl @@ -23,12 +23,12 @@ function _connection_obj_cost!(connection::Connection) model = connection.model - connection.obj.cost = JuMP.AffExpr(0.0) + connection.obj.cost = _isparametric(profile.cost) ? zero(JuMP.QuadExpr) : zero(JuMP.AffExpr) for t in get_T(connection.model) JuMP.add_to_expression!( connection.obj.cost, connection.var.flow[t], - _weight(model, t) * access(connection.cost, t, Float64), + _weight(model, t) * access(connection.cost, t), ) end diff --git a/src/core/decision.jl b/src/core/decision.jl index c0630b2d..57d3ab43 100644 --- a/src/core/decision.jl +++ b/src/core/decision.jl @@ -20,17 +20,17 @@ component's settings, as well as have associated costs. raw"""```{"mandatory": "no", "values": "numeric", "unit": "-", "default": "`0`"}``` Minimum size of the decision value (considered for each "unit" if count allows multiple "units"). """ - lb::_OptionalScalarInput = 0 + lb::Expression = @_default_expression(0.0) raw"""```{"mandatory": "no", "values": "numeric", "unit": "-", "default": "``+\\infty``"}``` Maximum size of the decision value (considered for each "unit" if count allows multiple "units"). """ - ub::_OptionalScalarInput = nothing + ub::Expression = @_default_expression(nothing) raw"""```{"mandatory": "no", "values": "numeric", "unit": "monetary (per value)", "default": "`0`"}``` Cost that the decision value induces, given as ``cost \cdot value``. """ - cost::_OptionalScalarInput = nothing + cost::Expression = @_default_expression(nothing) raw"""```{"mandatory": "no", "values": "numeric", "unit": "-", "default": "-"}``` If `mode: fixed`, this value is used as the fixed value of the decision. This can be useful if this `Decision` was @@ -165,6 +165,7 @@ include("decision/con_fixed.jl") include("decision/con_sos_value.jl") include("decision/con_sos1.jl") include("decision/con_sos2.jl") +include("decision/con_value_bounds.jl") include("decision/obj_fixed.jl") include("decision/obj_sos.jl") include("decision/obj_value.jl") @@ -183,7 +184,9 @@ function _construct_constraints!(decision::Decision) _decision_con_fixed!(decision) _decision_con_sos_value!(decision) _decision_con_sos1!(decision) - return _decision_con_sos2!(decision) + _decision_con_sos2!(decision) + _decision_con_value_bounds!(decision) + return nothing end function _construct_objective!(decision::Decision) diff --git a/src/core/decision/con_value_bounds.jl b/src/core/decision/con_value_bounds.jl new file mode 100644 index 00000000..440248cd --- /dev/null +++ b/src/core/decision/con_value_bounds.jl @@ -0,0 +1,24 @@ +@doc raw""" + _decision_con_value_bounds!(decision::Decision) + +to be added +""" +function _decision_con_value_bounds!(decision::Decision) + if !isnothing(decision.lb) + decision.con.value_lb = @constraint( + decision.model, + decision.var.value >= access(decision.lb), + base_name = make_base_name(decision, "value_lb") + ) + end + + if !isnothing(decision.ub) + decision.con.value_ub = @constraint( + decision.model, + decision.var.value <= access(decision.ub), + base_name = make_base_name(decision, "value_ub") + ) + end + + return nothing +end diff --git a/src/core/decision/obj_fixed.jl b/src/core/decision/obj_fixed.jl index 587dcce9..4fc13138 100644 --- a/src/core/decision/obj_fixed.jl +++ b/src/core/decision/obj_fixed.jl @@ -11,7 +11,7 @@ function _decision_obj_fixed!(decision::Decision) model = decision.model - decision.obj.fixed = JuMP.AffExpr(0.0) + decision.obj.fixed = _isparametric(profile.cost) ? zero(JuMP.QuadExpr) : zero(JuMP.AffExpr) if decision.mode === :sos1 for i in eachindex(decision.sos) if haskey(decision.sos[i], "fixed_cost") diff --git a/src/core/decision/obj_value.jl b/src/core/decision/obj_value.jl index ee7b2684..18336f81 100644 --- a/src/core/decision/obj_value.jl +++ b/src/core/decision/obj_value.jl @@ -13,7 +13,7 @@ function _decision_obj_value!(decision::Decision) end model = decision.model - decision.obj.value = decision.var.value * decision.cost + decision.obj.value = decision.var.value * access(decision.cost) push!(internal(model).model.objectives["total_cost"].terms, decision.obj.value) return nothing diff --git a/src/core/decision/var_value.jl b/src/core/decision/var_value.jl index e1457b83..f0f656b1 100644 --- a/src/core/decision/var_value.jl +++ b/src/core/decision/var_value.jl @@ -17,17 +17,6 @@ function _decision_var_value!(decision::Decision) if decision.mode === :fixed JuMP.fix(decision.var.value, decision.fixed_value) - else - if !isnothing(decision.ub) && (decision.lb == decision.ub) - JuMP.fix(decision.var.value, decision.lb) - else - if !isnothing(decision.lb) - JuMP.set_lower_bound(decision.var.value, decision.lb) - end - if !isnothing(decision.ub) - JuMP.set_upper_bound(decision.var.value, decision.ub) - end - end end return nothing diff --git a/src/core/expression.jl b/src/core/expression.jl index 86eee0eb..52447630 100644 --- a/src/core/expression.jl +++ b/src/core/expression.jl @@ -227,7 +227,15 @@ If the value of `my_exp` is a vector of `Float64`, the first call will succeed, empty::Bool = false parametric::Bool = false - value::Union{Nothing, JuMP.VariableRef, Vector{JuMP.VariableRef}, JuMP.AffExpr, Vector{JuMP.AffExpr}, Float64, Vector{Float64}} = nothing + value::Union{ + Nothing, + JuMP.VariableRef, + Vector{JuMP.VariableRef}, + JuMP.AffExpr, + Vector{JuMP.AffExpr}, + Float64, + Vector{Float64}, + } = nothing internal::Union{Nothing, NamedTuple} = nothing end @@ -280,7 +288,7 @@ function set_unknown!(e::Expression, value::Real) return nothing end -function set_unknown!(e::Expression, value::Vector{<: Real}) +function set_unknown!(e::Expression, value::Vector{<:Real}) if !e.parametric @critical "Only parametric expressions support `set_unknown`" end @@ -323,16 +331,38 @@ end function _convert_to_expression(model::JuMP.Model, @nospecialize(data::AbstractString)) if startswith(data, "\$") - # TODO: base_name = make_base_name(profile, "aux_value") + # NOTE (possible options are): + # - `$()`: A scalar unknown, defaulting to `0.0`. + # - `$(12.34)`: A scalar unknown, defaulting to `12.34`. + # - `$(t)`: A temporal unknown, defaulting to `0.0`. + # - `$(col@file)`: A temporal unknown, defaulting to the values in the column `col` of the file `file`. - if data == "\$()" - value = @variable(model, set = JuMP.Parameter(0.0)) - return Expression(; model, value, parametric=true) + if occursin("@", data) + # NOTE: Using `identity` here to "downcast" from `Union{Float64, Missing}` to `Float64`. + col, file = string.(split(data[3:(end - 1)], "@")) + default = identity.(_getfromcsv(model, file, col))::Vector{Float64} + value = @variable( + model, + [t = get_T(model)], + set = JuMP.Parameter(default[t]), + base_name = "p", + container = Array + ) # TODO: make_base_name(component, "some_field") + return Expression(; model, value, parametric=true, temporal=true) elseif data == "\$(t)" - value = @variable(model, [t = get_T(model)], set = JuMP.Parameter(0.0), container = Array) + value = @variable(model, [t = get_T(model)], set = JuMP.Parameter(0.0), base_name = "p", container = Array) # TODO: make_base_name(component, "some_field") return Expression(; model, value, parametric=true, temporal=true) + elseif data == "\$()" + value = @variable(model, set = JuMP.Parameter(0.0), base_name = "p") # TODO: make_base_name(component, "some_field") + return Expression(; model, value, parametric=true) else - @critical "Invalid expression string trying to create an unknown, either use `\$()` or `\$(t)`" data + # The assumption is that this looks like `$(17.4)`, being a scalar unknown, so we try to parse it. + try + value = @variable(model, set = JuMP.Parameter(parse(Float64, data[3:(end - 1)])), base_name = "p") # TODO: make_base_name(component, "some_field") + return Expression(; model, value, parametric=true) + catch + @critical "Invalid expression string trying to create an unknown, expected `\$(12.34)` or similar" data + end end end diff --git a/src/core/profile/obj_cost.jl b/src/core/profile/obj_cost.jl index bf13ecb2..a05a3c34 100644 --- a/src/core/profile/obj_cost.jl +++ b/src/core/profile/obj_cost.jl @@ -24,13 +24,9 @@ function _profile_obj_cost!(profile::Profile) # todo: this is inefficient: we are building up an AffExpr to add it to the objective; instead: add each term # todo: furthermore, this always calls VariableRef * Float, which is inefficient, and could be done in add_to_expression - profile.obj.cost = JuMP.AffExpr(0.0) + profile.obj.cost = _isparametric(profile.cost) ? zero(JuMP.QuadExpr) : zero(JuMP.AffExpr) for t in get_T(model) - JuMP.add_to_expression!( - profile.obj.cost, - profile.exp.value[t], - _weight(model, t) * access(profile.cost, t, Float64), - ) + JuMP.add_to_expression!(profile.obj.cost, profile.exp.value[t], _weight(model, t) * access(profile.cost, t)) end push!(internal(model).model.objectives["total_cost"].terms, profile.obj.cost) diff --git a/src/parser.jl b/src/parser.jl index 68e0c1c0..9eb6188e 100644 --- a/src/parser.jl +++ b/src/parser.jl @@ -58,8 +58,8 @@ function _parse_model!(model::JuMP.Model, filename::String) # Construct the objectives container & add all registered objectives. for (name, terms) in @config(model, optimization.objective.functions) internal(model).model.objectives[name] = ( - terms=Set{Union{JuMP.AffExpr, JuMP.VariableRef}}(), - expr=JuMP.AffExpr(0.0), + terms=Set{Union{JuMP.VariableRef, JuMP.AffExpr, JuMP.QuadExpr}}(), + expr=_is_parametric(model) ? zero(JuMP.QuadExpr) : zero(JuMP.AffExpr), constants=Vector{Float64}(), ) internal(model).aux._obj_terms[name] = terms @@ -461,13 +461,9 @@ function _parse_components!(model::JuMP.Model, @nospecialize(description::Dict{S # Convert to Symbol mode = Symbol(pop!(prop, "mode", :linear)) - lb = pop!(prop, "lb", 0) - ub = pop!(prop, "ub", nothing) - cost = pop!(prop, "cost", nothing) - - (lb isa AbstractString) && (lb = eval(Meta.parse(lb))) - (ub isa AbstractString) && (ub = eval(Meta.parse(ub))) - (cost isa AbstractString) && (cost = eval(Meta.parse(cost))) + lb = _convert_to_expression(model, pop!(prop, "lb", 0)) + ub = _convert_to_expression(model, pop!(prop, "ub", nothing)) + cost = _convert_to_expression(model, pop!(prop, "cost", nothing)) # Initialize. components[name] = Decision(; diff --git a/src/utils/general.jl b/src/utils/general.jl index af0ad158..4bb349bb 100644 --- a/src/utils/general.jl +++ b/src/utils/general.jl @@ -8,7 +8,7 @@ end expressions = _CoreComponentOptContainerDict{Union{JuMP.AffExpr, Vector{JuMP.AffExpr}}}() variables = _CoreComponentOptContainerDict{Union{JuMP.VariableRef, Vector{JuMP.VariableRef}}}() constraints = _CoreComponentOptContainerDict{Union{JuMP.ConstraintRef, Vector{<:JuMP.ConstraintRef}}}() # TODO: this clashes with a more specific definition of `ConstraintRef` in JuMP - objectives = _CoreComponentOptContainerDict{JuMP.AffExpr}() + objectives = _CoreComponentOptContainerDict{Union{JuMP.AffExpr, JuMP.QuadExpr}}() end """ From 8a7806488a40d5cc0c2329bcedfbe5d39cbe737d Mon Sep 17 00:00:00 2001 From: sstroemer Date: Sat, 22 Feb 2025 02:33:11 +0100 Subject: [PATCH 07/22] chore: formatting --- src/core/profile.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/profile.jl b/src/core/profile.jl index 7cc61922..ba002901 100644 --- a/src/core/profile.jl +++ b/src/core/profile.jl @@ -230,7 +230,7 @@ function _after_construct_variables!(profile::Profile) for t in get_T(model) _repr_t = internal(model).model.snapshots[t].is_representative ? t : - internal(model).model.snapshots[t].representative + internal(model).model.snapshots[t].representative if _isparametric(profile.value) val = access(profile.value, _repr_t)::JuMP.VariableRef From 3e8ca946a42def903bcd95838c0d8118af128065 Mon Sep 17 00:00:00 2001 From: sstroemer Date: Sat, 22 Feb 2025 02:54:23 +0100 Subject: [PATCH 08/22] feat: refactor to "modify", add "query", and add docstrings --- src/core/expression.jl | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/src/core/expression.jl b/src/core/expression.jl index 52447630..1247a19b 100644 --- a/src/core/expression.jl +++ b/src/core/expression.jl @@ -274,9 +274,14 @@ _isfixed(e::Expression) = ( _isempty(e::Expression) = e.empty::Bool _isparametric(e::Expression) = e.parametric::Bool -function set_unknown!(e::Expression, value::Real) +""" + modify!(e::Expression, value::Real) + +Set the value of a parametric `Expression` object to a scalar value `value`. +""" +function modify!(e::Expression, value::Real) if !e.parametric - @critical "Only parametric expressions support `set_unknown`" + @critical "Only parametric expressions support `modify`" end if e.temporal @@ -288,19 +293,41 @@ function set_unknown!(e::Expression, value::Real) return nothing end -function set_unknown!(e::Expression, value::Vector{<:Real}) +""" + modify!(e::Expression, value::Vector{<:Real}) + +Set the value of a parametric `Expression` object to a vector value `value`. +""" +function modify!(e::Expression, value::Vector{<:Real}) if !e.parametric - @critical "Only parametric expressions support `set_unknown`" + @critical "Only parametric expressions support `modify`" end if !e.temporal - @critical "Only temporal expressions support `set_unknown` with a vector-valued argument, use `\$(t)` instead of `\$()`" + @critical "Only temporal expressions support `modify` with a vector-valued argument, use `\$(t)` instead of `\$()`" end JuMP.set_parameter_value.(e.value, convert.(Float64, value)) return nothing end +""" + query(e::Expression) + +Query the value of a parametric `Expression` object. +""" +function query(e::Expression) + if !e.parametric + @critical "Only parametric expressions support `query`" + end + + if e.temporal + return JuMP.parameter_value.(e.value) + else + return JuMP.parameter_value(e.value) + end +end + @recompile_invalidations begin function Base.show(io::IO, e::Expression) str_show = """:: Expression ::""" From 968d7a211005cce83ef4fc85c78e3783f02ef53a Mon Sep 17 00:00:00 2001 From: sstroemer Date: Sat, 22 Feb 2025 02:54:50 +0100 Subject: [PATCH 09/22] fix: better handling of parametric Expressions in (some) Unit fields --- src/core/unit.jl | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/core/unit.jl b/src/core/unit.jl index 79762c3d..2d1cc18b 100644 --- a/src/core/unit.jl +++ b/src/core/unit.jl @@ -533,11 +533,25 @@ function _unit_capacity_limits(unit::Unit) # Get correct maximum. if !_isempty(unit.availability_factor) - max_conversion = min.(1.0, access(unit.availability_factor, NonEmptyNumericalExpressionValue)) + max_conversion = access(unit.availability_factor) + if !_isparametric(unit.availability_factor) + if any((>).(max_conversion, 1.0)) + @critical "Availability factor can not be greater than 1.0" unit = unit.name + end + else + if _isparametric(unit.capacity) + @critical "Parametric and are currently not supported" unit = unit.name + end + end elseif !_isempty(unit.availability) if !_isfixed(unit.capacity) @critical "Endogenuous and are currently not supported" unit = unit.name end + + if _isparametric(unit.availability) || _isparametric(unit.capacity) + @critical "Parametric and are currently not supported" unit = unit.name + end + max_conversion = min.( 1.0, From 4ee674c9305cc9002db43a2ad802e5e710126b69 Mon Sep 17 00:00:00 2001 From: sstroemer Date: Sat, 22 Feb 2025 02:58:20 +0100 Subject: [PATCH 10/22] assets: add example on parametric expressions --- .../51_parametric_expressions.iesopt.yaml | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 assets/examples/51_parametric_expressions.iesopt.yaml diff --git a/assets/examples/51_parametric_expressions.iesopt.yaml b/assets/examples/51_parametric_expressions.iesopt.yaml new file mode 100644 index 00000000..c1caa38b --- /dev/null +++ b/assets/examples/51_parametric_expressions.iesopt.yaml @@ -0,0 +1,92 @@ +config: + general: + version: + core: 2.7.0 + optimization: + problem_type: PARAMETRIC+LP + snapshots: + count: 168 + solver: + name: highs + results: + enabled: false + files: + data: example_data.csv + +carriers: + electricity: {} + gas: {} + co2: {} + +components: + node1: + type: Node + carrier: electricity + + node2: + type: Node + carrier: electricity + has_state: true + state_lb: $(0) + state_ub: $(10) + + conn: + type: Connection + capacity: $(5) + node_from: node1 + node_to: node2 + + # We could also use: `$(10) out:electricity` to model a parametric capacity. + plant_wind: + type: Unit + outputs: {electricity: node2} + conversion: ~ -> 1 electricity + capacity: 10 out:electricity + availability_factor: $(ex07_plant_wind_availability_factor@data) + + plant_gas: + type: Unit + inputs: {gas: gas_grid} + outputs: {electricity: node1, co2: total_co2} + conversion: 1 gas -> 0.4 electricity + 0.2 co2 + capacity: build:value out:electricity + + build: + type: Decision + lb: $(0) + ub: $(10) + cost: $(0) + + demand1: + type: Profile + carrier: electricity + node_from: node1 + value: $(ex07_demand1_value@data) + + demand2: + type: Profile + carrier: electricity + node_from: node2 + value: $(ex07_demand2_value@data) + + gas_grid: + type: Node + carrier: gas + + total_co2: + type: Node + carrier: co2 + + create_gas: + type: Profile + carrier: gas + node_to: gas_grid + mode: create + cost: $(50) + + co2_emissions: + type: Profile + carrier: co2 + node_from: total_co2 + mode: destroy + cost: $(100.) From 1b4dc1bb40ec5e6b7d8413f5426810033ebd912b Mon Sep 17 00:00:00 2001 From: sstroemer Date: Sat, 22 Feb 2025 03:23:00 +0100 Subject: [PATCH 11/22] chore: formatting --- src/core/unit.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/unit.jl b/src/core/unit.jl index 2d1cc18b..00e75774 100644 --- a/src/core/unit.jl +++ b/src/core/unit.jl @@ -550,7 +550,7 @@ function _unit_capacity_limits(unit::Unit) if _isparametric(unit.availability) || _isparametric(unit.capacity) @critical "Parametric and are currently not supported" unit = unit.name - end + end max_conversion = min.( From f8973f4656de9b4a17f1be02c65400ae00cb6594 Mon Sep 17 00:00:00 2001 From: sstroemer Date: Sat, 22 Feb 2025 03:23:54 +0100 Subject: [PATCH 12/22] feat: implement proper names for parameters, improving `print(model)` --- src/IESopt.jl | 6 ----- src/core/connection.jl | 2 +- src/core/expression.jl | 28 ++++++++++++--------- src/parser.jl | 55 +++++++++++++++++++++++++++--------------- 4 files changed, 53 insertions(+), 38 deletions(-) diff --git a/src/IESopt.jl b/src/IESopt.jl index a69b3b1c..68bf7cdf 100644 --- a/src/IESopt.jl +++ b/src/IESopt.jl @@ -20,12 +20,6 @@ include("docify/docify.jl") function _build_model!(model::JuMP.Model) @info "[build] Begin creating JuMP formulation from components" - if @config(model, general.performance.string_names, Bool) != model.set_string_names_on_creation - new_val = @config(model, general.performance.string_names, Bool) - @debug "Overwriting `string_names_on_creation` to `$(new_val)` based on config" - JuMP.set_string_names_on_creation(model, new_val) - end - # This specifies the order in which components are built. This ensures that model parts that are used later on, are # already initialized (e.g. constructing a constraint may use expressions and variables). build_order = [ diff --git a/src/core/connection.jl b/src/core/connection.jl index 8fab6fc9..419868d4 100644 --- a/src/core/connection.jl +++ b/src/core/connection.jl @@ -201,7 +201,7 @@ function _prepare!(connection::Connection) if _isempty(connection.capacity) # Only calculate capacity if it is not given by the user - connection.capacity = _convert_to_expression(model, connection.pf_V * connection.pf_I) + connection.capacity = _convert_to_expression(model, connection.pf_V * connection.pf_I, "") end # todo: convert B1 end diff --git a/src/core/expression.jl b/src/core/expression.jl index 1247a19b..ad394f32 100644 --- a/src/core/expression.jl +++ b/src/core/expression.jl @@ -347,16 +347,16 @@ end end end -_convert_to_expression(model::JuMP.Model, ::Nothing) = Expression(; model, empty=true) -_convert_to_expression(model::JuMP.Model, data::Real) = Expression(; model, value=convert(Float64, data)) -_convert_to_expression(model::JuMP.Model, data::Vector{<:Real}) = +_convert_to_expression(model::JuMP.Model, ::Nothing, ::String) = Expression(; model, empty=true) +_convert_to_expression(model::JuMP.Model, data::Real, ::String) = Expression(; model, value=convert(Float64, data)) +_convert_to_expression(model::JuMP.Model, data::Vector{<:Real}, ::String) = Expression(; model, value=convert.(Float64, data), temporal=true) macro _default_expression(value) - return esc(:(_convert_to_expression(model, $value))) + return esc(:(_convert_to_expression(model, $value, ""))) end -function _convert_to_expression(model::JuMP.Model, @nospecialize(data::AbstractString)) +function _convert_to_expression(model::JuMP.Model, @nospecialize(data::AbstractString), base_name::String) if startswith(data, "\$") # NOTE (possible options are): # - `$()`: A scalar unknown, defaulting to `0.0`. @@ -372,20 +372,26 @@ function _convert_to_expression(model::JuMP.Model, @nospecialize(data::AbstractS model, [t = get_T(model)], set = JuMP.Parameter(default[t]), - base_name = "p", + base_name = base_name, container = Array - ) # TODO: make_base_name(component, "some_field") + ) return Expression(; model, value, parametric=true, temporal=true) elseif data == "\$(t)" - value = @variable(model, [t = get_T(model)], set = JuMP.Parameter(0.0), base_name = "p", container = Array) # TODO: make_base_name(component, "some_field") + value = @variable( + model, + [t = get_T(model)], + set = JuMP.Parameter(0.0), + base_name = base_name, + container = Array + ) return Expression(; model, value, parametric=true, temporal=true) elseif data == "\$()" - value = @variable(model, set = JuMP.Parameter(0.0), base_name = "p") # TODO: make_base_name(component, "some_field") + value = @variable(model, set = JuMP.Parameter(0.0), base_name = base_name) return Expression(; model, value, parametric=true) else # The assumption is that this looks like `$(17.4)`, being a scalar unknown, so we try to parse it. try - value = @variable(model, set = JuMP.Parameter(parse(Float64, data[3:(end - 1)])), base_name = "p") # TODO: make_base_name(component, "some_field") + value = @variable(model, set = JuMP.Parameter(parse(Float64, data[3:(end - 1)])), base_name = base_name) return Expression(; model, value, parametric=true) catch @critical "Invalid expression string trying to create an unknown, expected `\$(12.34)` or similar" data @@ -449,7 +455,7 @@ end function _prepare(e::Expression; default::Float64) if e.empty - return _convert_to_expression(e.model, default)::Expression + return _convert_to_expression(e.model, default, "")::Expression else return e::Expression end diff --git a/src/parser.jl b/src/parser.jl index 9eb6188e..4b0e3a1b 100644 --- a/src/parser.jl +++ b/src/parser.jl @@ -26,6 +26,13 @@ function _parse_model!(model::JuMP.Model, filename::String) @warn "The configured `version.core` (v$(v_core)) in the configuration file is not identical with the current version of `IESopt.jl` (v$(v_curr)); be aware that even bug fixes might change the results and therefore should be considered BREAKING for your project" end + # Already set `string_names_on_creation`, since we rely on it during component parsing (for parameter names). + if @config(model, general.performance.string_names, Bool) != model.set_string_names_on_creation + new_val = @config(model, general.performance.string_names, Bool) + @debug "Overwriting `string_names_on_creation` to `$(new_val)` based on config" + JuMP.set_string_names_on_creation(model, new_val) + end + # Pre-load all registered files. merge!(internal(model).input.files, _parse_inputfiles(model)) if !isempty(internal(model).input.files) @@ -234,6 +241,9 @@ function _parse_components!(model::JuMP.Model, @nospecialize(description::Dict{S has_invalid_component_name = false + # Dynamic helper function for base names, before we have actual components (=> can't use `make_base_name`). + mkbn(n::String, sfx::String) = JuMP.set_string_names_on_creation(model) ? "$(n).par.$(sfx)" : "" + for (desc, prop) in description if _parse_bool(model, pop!(prop, "disabled", false)) || !_parse_bool(model, pop!(prop, "enabled", true)) @critical "[parse] Disabled components should not end up in parse" @@ -290,8 +300,8 @@ function _parse_components!(model::JuMP.Model, @nospecialize(description::Dict{S carrier = internal(model).model.carriers[pop!(prop, "carrier")] # Convert to _Expression. - state_lb = _convert_to_expression(model, pop!(prop, "state_lb", nothing)) - state_ub = _convert_to_expression(model, pop!(prop, "state_ub", nothing)) + state_lb = _convert_to_expression(model, pop!(prop, "state_lb", nothing), mkbn(name, "state_lb")) + state_ub = _convert_to_expression(model, pop!(prop, "state_ub", nothing), mkbn(name, "state_ub")) # Convert to Symbol state_cyclic = Symbol(pop!(prop, "state_cyclic", :eq)) @@ -340,12 +350,12 @@ function _parse_components!(model::JuMP.Model, @nospecialize(description::Dict{S end # Convert to _Expression. - lb = _convert_to_expression(model, pop!(prop, "lb", nothing)) - ub = _convert_to_expression(model, pop!(prop, "ub", nothing)) - capacity = _convert_to_expression(model, pop!(prop, "capacity", nothing)) - cost = _convert_to_expression(model, pop!(prop, "cost", nothing)) - loss = _convert_to_expression(model, pop!(prop, "loss", nothing)) - delay = _convert_to_expression(model, pop!(prop, "delay", nothing)) + lb = _convert_to_expression(model, pop!(prop, "lb", nothing), mkbn(name, "lb")) + ub = _convert_to_expression(model, pop!(prop, "ub", nothing), mkbn(name, "ub")) + capacity = _convert_to_expression(model, pop!(prop, "capacity", nothing), mkbn(name, "capacity")) + cost = _convert_to_expression(model, pop!(prop, "cost", nothing), mkbn(name, "cost")) + loss = _convert_to_expression(model, pop!(prop, "loss", nothing), mkbn(name, "loss")) + delay = _convert_to_expression(model, pop!(prop, "delay", nothing), mkbn(name, "delay")) # Convert to Symbol loss_mode = Symbol(pop!(prop, "loss_mode", :to)) @@ -371,10 +381,10 @@ function _parse_components!(model::JuMP.Model, @nospecialize(description::Dict{S carrier = internal(model).model.carriers[pop!(prop, "carrier")] # Convert to _Expression. - value = _convert_to_expression(model, pop!(prop, "value", nothing)) - lb = _convert_to_expression(model, pop!(prop, "lb", nothing)) - ub = _convert_to_expression(model, pop!(prop, "ub", nothing)) - cost = _convert_to_expression(model, pop!(prop, "cost", nothing)) + value = _convert_to_expression(model, pop!(prop, "value", nothing), mkbn(name, "value")) + lb = _convert_to_expression(model, pop!(prop, "lb", nothing), mkbn(name, "lb")) + ub = _convert_to_expression(model, pop!(prop, "ub", nothing), mkbn(name, "ub")) + cost = _convert_to_expression(model, pop!(prop, "cost", nothing), mkbn(name, "cost")) # Convert to Symbol mode = Symbol(pop!(prop, "mode", :fixed)) @@ -425,11 +435,16 @@ function _parse_components!(model::JuMP.Model, @nospecialize(description::Dict{S end # Convert to _Expression. - availability = _convert_to_expression(model, pop!(prop, "availability", nothing)) - availability_factor = _convert_to_expression(model, pop!(prop, "availability_factor", nothing)) - unit_count = _convert_to_expression(model, pop!(prop, "unit_count", 1)) - capacity = _convert_to_expression(model, _capacity) - marginal_cost = _convert_to_expression(model, _marginal_cost) + availability = + _convert_to_expression(model, pop!(prop, "availability", nothing), mkbn(name, "availability")) + availability_factor = _convert_to_expression( + model, + pop!(prop, "availability_factor", nothing), + mkbn(name, "availability_factor"), + ) + unit_count = _convert_to_expression(model, pop!(prop, "unit_count", 1), mkbn(name, "unit_count")) + capacity = _convert_to_expression(model, _capacity, mkbn(name, "capacity")) + marginal_cost = _convert_to_expression(model, _marginal_cost, mkbn(name, "marginal_cost")) # Convert to Symbol unit_commitment = Symbol(pop!(prop, "unit_commitment", :off)) @@ -461,9 +476,9 @@ function _parse_components!(model::JuMP.Model, @nospecialize(description::Dict{S # Convert to Symbol mode = Symbol(pop!(prop, "mode", :linear)) - lb = _convert_to_expression(model, pop!(prop, "lb", 0)) - ub = _convert_to_expression(model, pop!(prop, "ub", nothing)) - cost = _convert_to_expression(model, pop!(prop, "cost", nothing)) + lb = _convert_to_expression(model, pop!(prop, "lb", 0), mkbn(name, "lb")) + ub = _convert_to_expression(model, pop!(prop, "ub", nothing), mkbn(name, "ub")) + cost = _convert_to_expression(model, pop!(prop, "cost", nothing), mkbn(name, "cost")) # Initialize. components[name] = Decision(; From 5cf4d5f312bc0f6da75c894e47aab5cc7f476474 Mon Sep 17 00:00:00 2001 From: sstroemer Date: Sat, 22 Feb 2025 03:32:18 +0100 Subject: [PATCH 13/22] test: cover parametric expression example --- test/src/examples.jl | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/test/src/examples.jl b/test/src/examples.jl index ccc8be12..24ac43c9 100644 --- a/test/src/examples.jl +++ b/test/src/examples.jl @@ -249,3 +249,32 @@ end @test river.exp.out[i] == first_inflow + second_inflow end end + +@testitem "51_parametric_expressions" tags = [:examples] setup = [TestExampleModule] begin + fn_non_parametric = String(Assets.get_path("examples", "07_csv_filestorage.iesopt.yaml")) + fn_parametric = String(Assets.get_path("examples", "51_parametric_expressions.iesopt.yaml")) + + model = generate!(fn_non_parametric; config=Dict("optimization.snapshots.count" => 168)) + optimize!(model) + obj_val = JuMP.objective_value(model) + + model = generate!(fn_parametric) + optimize!(model) + @test JuMP.objective_value(model) ≈ obj_val atol = 1e-3 + + modify!(get_component(model, "build").cost, 100) + optimize!(model) + @test JuMP.objective_value(model) ≈ (obj_val + 166.5) atol = 1e-3 + + af = query(get_component(model, "plant_wind").availability_factor) + @test maximum(af) ≈ 0.99 atol = 1e-3 + @test minimum(af) ≈ 0.00 atol = 1e-3 + + modify!(get_component(model, "plant_wind").availability_factor, af .* 0.95) + optimize!(model) + @test JuMP.objective_value(model) > (obj_val + 166.5) + + modify!(get_component(model, "plant_wind").availability_factor, af) + optimize!(model) + @test JuMP.objective_value(model) ≈ (obj_val + 166.5) atol = 1e-3 +end From 35636af58199ce115c1dd10b0022933457580d88 Mon Sep 17 00:00:00 2001 From: sstroemer Date: Sat, 22 Feb 2025 03:41:28 +0100 Subject: [PATCH 14/22] fix: proper check for empty expression --- src/core/decision/con_value_bounds.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/decision/con_value_bounds.jl b/src/core/decision/con_value_bounds.jl index 440248cd..ecd1f9e2 100644 --- a/src/core/decision/con_value_bounds.jl +++ b/src/core/decision/con_value_bounds.jl @@ -4,7 +4,7 @@ to be added """ function _decision_con_value_bounds!(decision::Decision) - if !isnothing(decision.lb) + if !_isempty(decision.lb) decision.con.value_lb = @constraint( decision.model, decision.var.value >= access(decision.lb), @@ -12,7 +12,7 @@ function _decision_con_value_bounds!(decision::Decision) ) end - if !isnothing(decision.ub) + if !_isempty(decision.ub) decision.con.value_ub = @constraint( decision.model, decision.var.value <= access(decision.ub), From 9988715e77ccb254ab502b6cd3b96ee0f5531ce8 Mon Sep 17 00:00:00 2001 From: sstroemer Date: Sat, 22 Feb 2025 03:41:44 +0100 Subject: [PATCH 15/22] fix: missing JuMP import --- test/src/examples.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/src/examples.jl b/test/src/examples.jl index 24ac43c9..77825597 100644 --- a/test/src/examples.jl +++ b/test/src/examples.jl @@ -250,7 +250,7 @@ end end end -@testitem "51_parametric_expressions" tags = [:examples] setup = [TestExampleModule] begin +@testitem "51_parametric_expressions" tags = [:examples] setup = [Dependencies, TestExampleModule] begin fn_non_parametric = String(Assets.get_path("examples", "07_csv_filestorage.iesopt.yaml")) fn_parametric = String(Assets.get_path("examples", "51_parametric_expressions.iesopt.yaml")) From f4eee91cb9a1b1c5430684f897298c2f0457103a Mon Sep 17 00:00:00 2001 From: sstroemer Date: Sat, 22 Feb 2025 03:45:35 +0100 Subject: [PATCH 16/22] test: re-enable JET test set --- test/runtests.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index 830219ce..fd963843 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -15,7 +15,7 @@ include("src/examples.jl") end @testset "JET.jl" begin - # JET.test_package(IESopt; target_modules=(IESopt,)) + JET.test_package(IESopt; target_modules=(IESopt,)) end end From 031b2afe7ba8aad03d8a255359f0330bb338deed Mon Sep 17 00:00:00 2001 From: sstroemer Date: Sat, 22 Feb 2025 03:46:31 +0100 Subject: [PATCH 17/22] fix: bump logging severity to prevent users overlooking possible Profile misconfiguration --- src/core/profile.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/profile.jl b/src/core/profile.jl index ba002901..1632c490 100644 --- a/src/core/profile.jl +++ b/src/core/profile.jl @@ -145,8 +145,8 @@ function _isvalid(profile::Profile) end if (profile.mode === :create) || (profile.mode === :destroy) - !_isempty(profile.lb) && (@warn "Setting is ignored" profile = profile.name mode = profile.mode) - !_isempty(profile.ub) && (@warn "Setting is ignored" profile = profile.name mode = profile.mode) + !_isempty(profile.lb) && (@error "Setting is ignored" profile = profile.name mode = profile.mode) + !_isempty(profile.ub) && (@error "Setting is ignored" profile = profile.name mode = profile.mode) end if !(profile.mode in [:fixed, :create, :destroy, :ranged]) From 40a97732932f438e43646b66139d1514bc9806aa Mon Sep 17 00:00:00 2001 From: sstroemer Date: Sat, 22 Feb 2025 04:29:19 +0100 Subject: [PATCH 18/22] chore: remove leftover todo --- src/IESopt.jl | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/IESopt.jl b/src/IESopt.jl index 68bf7cdf..7ce4681b 100644 --- a/src/IESopt.jl +++ b/src/IESopt.jl @@ -767,9 +767,6 @@ function _optimize!(model::JuMP.Model; @nospecialize(kwargs...)) end else @info "[optimize] Passing model to solver" - end - - # TODO: if the objective expression is still "Quad" due to a parametric model, we could substitute it with a affine expression (maybe in the hook?) JuMP.optimize!(model; kwargs...) From 2ac990ab5f037d90e42462ef97ef67e38e362318 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Str=C3=B6mer?= <8915976+sstroemer@users.noreply.github.com> Date: Sun, 23 Feb 2025 10:41:45 +0100 Subject: [PATCH 19/22] fix: restore deleted `end` --- src/IESopt.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/IESopt.jl b/src/IESopt.jl index 7ce4681b..82db9e41 100644 --- a/src/IESopt.jl +++ b/src/IESopt.jl @@ -767,6 +767,7 @@ function _optimize!(model::JuMP.Model; @nospecialize(kwargs...)) end else @info "[optimize] Passing model to solver" + end JuMP.optimize!(model; kwargs...) From 34407ed2f3374ad417dd405020347d6810c09f24 Mon Sep 17 00:00:00 2001 From: sstroemer Date: Sun, 23 Feb 2025 11:07:04 +0100 Subject: [PATCH 20/22] feat: allow parsing basic "expression strings" for fixed scalar parameterized expressions --- src/core/expression.jl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/core/expression.jl b/src/core/expression.jl index ad394f32..2e57f316 100644 --- a/src/core/expression.jl +++ b/src/core/expression.jl @@ -391,7 +391,11 @@ function _convert_to_expression(model::JuMP.Model, @nospecialize(data::AbstractS else # The assumption is that this looks like `$(17.4)`, being a scalar unknown, so we try to parse it. try - value = @variable(model, set = JuMP.Parameter(parse(Float64, data[3:(end - 1)])), base_name = base_name) + value = @variable( + model, + set = JuMP.Parameter(convert(Float64, eval(JuliaSyntax.parsestmt(Expr, data[3:(end - 1)])))), + base_name = base_name + ) return Expression(; model, value, parametric=true) catch @critical "Invalid expression string trying to create an unknown, expected `\$(12.34)` or similar" data From cdffe22de95520ea27f9c513289c354c6346972d Mon Sep 17 00:00:00 2001 From: sstroemer Date: Sun, 23 Feb 2025 11:55:12 +0100 Subject: [PATCH 21/22] feat: skip QP-to-LP optimizer hook for QPs; add support for QP as problem type --- src/IESopt.jl | 2 +- src/config/config.jl | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/IESopt.jl b/src/IESopt.jl index 82db9e41..35d86e97 100644 --- a/src/IESopt.jl +++ b/src/IESopt.jl @@ -127,7 +127,7 @@ function _build_model!(model::JuMP.Model) end if !_is_multiobjective(model) - if !_is_parametric(model) + if !_is_parametric(model) || _is_qp(model) current_objective = @config(model, optimization.objective.current) isnothing(current_objective) && @critical "[build] Missing an active objective" @objective(model, Min, internal(model).model.objectives[current_objective].expr) diff --git a/src/config/config.jl b/src/config/config.jl index 4abda6e7..b05d5639 100644 --- a/src/config/config.jl +++ b/src/config/config.jl @@ -80,4 +80,5 @@ _has_representative_snapshots(model::JuMP.Model) = false # TODO _is_multiobjective(model::JuMP.Model) = (:mo in @config(model, optimization.problem_type))::Bool _is_lp(model::JuMP.Model) = (:lp in @config(model, optimization.problem_type))::Bool _is_milp(model::JuMP.Model) = (:milp in @config(model, optimization.problem_type))::Bool +_is_qp(model::JuMP.Model) = (:qp in @config(model, optimization.problem_type))::Bool _is_parametric(model::JuMP.Model) = (:parametric in @config(model, optimization.problem_type))::Bool From c1d84a1dd5d945d7414e5c2a2027d7da10c141de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Str=C3=B6mer?= Date: Mon, 10 Mar 2025 18:45:37 +0100 Subject: [PATCH 22/22] fix: connection objective cost --- src/core/connection/obj_cost.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/connection/obj_cost.jl b/src/core/connection/obj_cost.jl index b49c5870..b231b901 100644 --- a/src/core/connection/obj_cost.jl +++ b/src/core/connection/obj_cost.jl @@ -23,7 +23,7 @@ function _connection_obj_cost!(connection::Connection) model = connection.model - connection.obj.cost = _isparametric(profile.cost) ? zero(JuMP.QuadExpr) : zero(JuMP.AffExpr) + connection.obj.cost = _isparametric(connection.cost) ? zero(JuMP.QuadExpr) : zero(JuMP.AffExpr) for t in get_T(connection.model) JuMP.add_to_expression!( connection.obj.cost,