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.) diff --git a/src/IESopt.jl b/src/IESopt.jl index dbd2d492..35d86e97 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 = [ @@ -133,9 +127,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) || _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) + 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 +145,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) diff --git a/src/config/config.jl b/src/config/config.jl index aad384af..b05d5639 100644 --- a/src/config/config.jl +++ b/src/config/config.jl @@ -80,3 +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 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/connection/obj_cost.jl b/src/core/connection/obj_cost.jl index 38e153a6..b231b901 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(connection.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..ecd1f9e2 --- /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 !_isempty(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 !_isempty(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 e310c1f8..2e57f316 100644 --- a/src/core/expression.jl +++ b/src/core/expression.jl @@ -225,8 +225,17 @@ 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 - - value::Union{Nothing, JuMP.VariableRef, JuMP.AffExpr, Vector{JuMP.AffExpr}, Float64, Vector{Float64}} = nothing + parametric::Bool = false + + 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 +272,61 @@ _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 + +""" + 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 `modify`" + 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 + +""" + 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 `modify`" + end + + if !e.temporal + @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) @@ -283,16 +347,62 @@ _isempty(e::Expression) = e.empty::Bool 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`. + # - `$(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 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 = base_name, + container = Array + ) + 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 = base_name, + container = Array + ) + return Expression(; model, value, parametric=true, temporal=true) + elseif data == "\$()" + 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(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 + end + end + end + parsed = _parse_expression(model, data, _GeneralExpressionType()) if hasproperty(parsed, :val) @@ -349,7 +459,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 @@ -372,6 +482,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 diff --git a/src/core/profile.jl b/src/core/profile.jl index 50f46807..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]) @@ -231,12 +231,12 @@ function _after_construct_variables!(profile::Profile) _repr_t = internal(model).model.snapshots[t].is_representative ? t : internal(model).model.snapshots[t].representative - val = access(profile.value, _repr_t, Float64) - 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 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/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) diff --git a/src/core/unit.jl b/src/core/unit.jl index 79762c3d..00e75774 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, diff --git a/src/parser.jl b/src/parser.jl index 68e0c1c0..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) @@ -58,8 +65,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 @@ -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,13 +476,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), 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(; 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 """ 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 diff --git a/test/src/examples.jl b/test/src/examples.jl index bf5b30d2..ba8306ab 100644 --- a/test/src/examples.jl +++ b/test/src/examples.jl @@ -250,6 +250,35 @@ end end end +@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")) + + 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 + @testitem "52_simple_ev" tags = [:examples] setup = [Dependencies, TestExampleModule] begin model = TestExampleModule.check(; obj=5.7895)