diff --git a/Project.toml b/Project.toml index e749a78..0c2127c 100644 --- a/Project.toml +++ b/Project.toml @@ -23,18 +23,17 @@ TimerOutputs = "a759f4b9-e2f1-59dc-863e-4aeb61b1ea8f" [weakdeps] PowerFlows = "94fada2c-0ca5-4b90-a1fb-4bc5b59ccfc7" +[sources] +InfrastructureSystems = {rev = "IS4", url = "https://github.com/Sienna-Platform/InfrastructureSystems.jl"} +PowerSystems = {rev = "psy6", url = "https://github.com/Sienna-Platform/PowerSystems.jl"} +InfrastructureOptimizationModels = {rev = "ac/hvdc-vsc", url = "https://github.com/Sienna-Platform/InfrastructureOptimizationModels.jl"} + [extensions] PowerFlowsExt = "PowerFlows" -[sources] -InfrastructureSystems = {url = "https://github.com/NREL-Sienna/InfrastructureSystems.jl", rev = "IS4"} -PowerSystems = {url = "https://github.com/NREL-Sienna/PowerSystems.jl", rev = "psy6"} -InfrastructureOptimizationModels = {url = "https://github.com/NREL-Sienna/InfrastructureOptimizationModels.jl", rev = "lk/pom-test-fixes"} - [compat] Dates = "1" DocStringExtensions = "~0.8, ~0.9" -InfrastructureOptimizationModels = "0.1" InfrastructureSystems = "3" InteractiveUtils = "1.11.0" JuMP = "^1.28" diff --git a/src/PowerOperationsModels.jl b/src/PowerOperationsModels.jl index 27411c1..cef1246 100644 --- a/src/PowerOperationsModels.jl +++ b/src/PowerOperationsModels.jl @@ -213,6 +213,7 @@ include("common_models/add_to_expression.jl") include("common_models/add_parameters.jl") include("common_models/make_system_expressions.jl") include("common_models/reserve_range_constraints.jl") +include("common_models/quadratic_converter_loss.jl") # Market bid cost plumbing (PSY orchestration moved out of IOM). Must be included # before device-specific files that reference MBC_TYPES / IEC_TYPES. @@ -505,24 +506,16 @@ export PostContingencyActivePowerReserveDeploymentVariable # HVDC Variables export DCVoltage export DCLineCurrent -export ConverterPowerDirection export ConverterCurrent -export SquaredConverterCurrent -export InterpolationSquaredCurrentVariable -export InterpolationBinarySquaredCurrentVariable -export ConverterPositiveCurrent -export ConverterNegativeCurrent -export SquaredDCVoltage -export InterpolationSquaredVoltageVariable -export InterpolationBinarySquaredVoltageVariable -export AuxBilinearConverterVariable -export AuxBilinearSquaredConverterVariable -export InterpolationSquaredBilinearVariable -export InterpolationBinarySquaredBilinearVariable +export PositiveCurrent +export NegativeCurrent +export CurrentDirection export HVDCFlowDirectionVariable export HVDCLosses -export ConverterDCPower -export ConverterCurrentDirection +export HVDCFromDCVoltage +export HVDCToDCVoltage +export HVDCReactivePowerFromVariable +export HVDCReactivePowerToVariable # Load Variables export ShiftUpActivePowerVariable @@ -770,10 +763,14 @@ export HVDCTwoTerminalLossless export HVDCTwoTerminalDispatch export HVDCTwoTerminalPiecewiseLoss export HVDCTwoTerminalLCC +export HVDCTwoTerminalVSC +export HVDCTwoTerminalVSCBin2 # Converter Formulations export LosslessConverter export LinearLossConverter +export AbstractQuadraticLossConverter +export Bin2QuadraticLossConverter export QuadraticLossConverter # DC Line Formulations diff --git a/src/ac_transmission_models/branch_constructor.jl b/src/ac_transmission_models/branch_constructor.jl index 568b072..3da7d40 100644 --- a/src/ac_transmission_models/branch_constructor.jl +++ b/src/ac_transmission_models/branch_constructor.jl @@ -1670,3 +1670,180 @@ function construct_device!( add_feedforward_constraints!(container, device_model, devices) return end + +############################################################################ +####################### Two-Terminal VSC HVDC Construct #################### +############################################################################ + +# Quadratic / bilinear approximation traits — same scheme used by the MT +# converter formulations. +_quad_config(::Type{HVDCTwoTerminalVSC}) = IOM.NoQuadApproxConfig() +_quad_config(::Type{HVDCTwoTerminalVSCBin2}) = + IOM.SolverSOS2QuadConfig(DEFAULT_INTERPOLATION_LENGTH) +_bilinear_config(::Type{HVDCTwoTerminalVSC}) = IOM.NoBilinearApproxConfig() +_bilinear_config(::Type{HVDCTwoTerminalVSCBin2}) = + IOM.Bin2Config(IOM.SolverSOS2QuadConfig(DEFAULT_INTERPOLATION_LENGTH)) + +# IOM's quadratic/bilinear approximation helpers take per-device bounds as +# `Vector{IOM.MinMax}` (one (min, max) pair per device, same order as `devices`). +function _vsc_v_from_bounds(devices) + n = length(devices) + bounds = Vector{IOM.MinMax}(undef, n) + for (k, d) in enumerate(devices) + lims = PSY.get_voltage_limits_from(d) + bounds[k] = IOM.MinMax((min = lims.min, max = lims.max)) + end + return bounds +end + +function _vsc_v_to_bounds(devices) + n = length(devices) + bounds = Vector{IOM.MinMax}(undef, n) + for (k, d) in enumerate(devices) + lims = PSY.get_voltage_limits_to(d) + bounds[k] = IOM.MinMax((min = lims.min, max = lims.max)) + end + return bounds +end + +function _vsc_i_bounds(devices) + n = length(devices) + bounds = Vector{IOM.MinMax}(undef, n) + for (k, d) in enumerate(devices) + i_max = _vsc_shared_i_max(d) + bounds[k] = IOM.MinMax((min = -i_max, max = i_max)) + end + return bounds +end + +function construct_device!( + container::OptimizationContainer, + sys::PSY.System, + ::ArgumentConstructStage, + device_model::DeviceModel{PSY.TwoTerminalVSCLine, F}, + network_model::NetworkModel{<:AbstractPowerModel}, +) where {F <: AbstractTwoTerminalVSCFormulation} + devices = get_available_components(device_model, sys) + + add_variables!(container, FlowActivePowerFromToVariable, devices, F) + add_variables!(container, FlowActivePowerToFromVariable, devices, F) + add_variables!(container, DCLineCurrentFlowVariable, devices, F) + add_variables!(container, HVDCFromDCVoltage, devices, F) + add_variables!(container, HVDCToDCVoltage, devices, F) + + _maybe_add_reactive_power_variables!(container, devices, device_model, network_model) + + if _use_linear_loss(F, device_model) + ll_devices = _devices_with_linear_loss(devices) + if isempty(ll_devices) + @warn "use_linear_loss is enabled but every TwoTerminalVSCLine has zero proportional loss terms; no linear-loss variables/constraints will be added." + else + _add_abs_value_decomposition_variables!(container, ll_devices, device_model) + end + end + + add_to_expression!( + container, ActivePowerBalance, FlowActivePowerFromToVariable, + devices, device_model, network_model, + ) + add_to_expression!( + container, ActivePowerBalance, FlowActivePowerToFromVariable, + devices, device_model, network_model, + ) + + add_feedforward_arguments!(container, device_model, devices) + return +end + +function construct_device!( + container::OptimizationContainer, + sys::PSY.System, + ::ModelConstructStage, + device_model::DeviceModel{PSY.TwoTerminalVSCLine, F}, + network_model::NetworkModel{<:AbstractPowerModel}, +) where {F <: AbstractTwoTerminalVSCFormulation} + devices = get_available_components(device_model, sys) + time_steps = get_time_steps(container) + line_names = [PSY.get_name(d) for d in devices] + + v_f_var = get_variable(container, HVDCFromDCVoltage, PSY.TwoTerminalVSCLine) + v_t_var = get_variable(container, HVDCToDCVoltage, PSY.TwoTerminalVSCLine) + i_var = get_variable(container, DCLineCurrentFlowVariable, PSY.TwoTerminalVSCLine) + + v_f_bounds = _vsc_v_from_bounds(devices) + v_t_bounds = _vsc_v_to_bounds(devices) + i_bounds = _vsc_i_bounds(devices) + + quad_cfg, bilin_cfg = _quad_config(F), _bilinear_config(F) + + v_f_sq_expr = IOM._add_quadratic_approx!( + quad_cfg, container, PSY.TwoTerminalVSCLine, + line_names, time_steps, v_f_var, v_f_bounds, "v_f_sq", + ) + v_t_sq_expr = IOM._add_quadratic_approx!( + quad_cfg, container, PSY.TwoTerminalVSCLine, + line_names, time_steps, v_t_var, v_t_bounds, "v_t_sq", + ) + i_sq_expr = IOM._add_quadratic_approx!( + quad_cfg, container, PSY.TwoTerminalVSCLine, + line_names, time_steps, i_var, i_bounds, "i_sq", + ) + + IOM._add_bilinear_approx!( + bilin_cfg, container, PSY.TwoTerminalVSCLine, + line_names, time_steps, + v_f_sq_expr, i_sq_expr, v_f_var, i_var, + v_f_bounds, i_bounds, "vi_ft", + ) + IOM._add_bilinear_approx!( + bilin_cfg, container, PSY.TwoTerminalVSCLine, + line_names, time_steps, + v_t_sq_expr, i_sq_expr, v_t_var, i_var, + v_t_bounds, i_bounds, "vi_tf", + ) + + add_constraints!( + container, HVDCCableOhmsLawConstraint, devices, device_model, network_model, + ) + add_constraints!( + container, HVDCVSCConverterPowerConstraint, devices, device_model, network_model, + ) + _maybe_add_reactive_power_constraints!(container, devices, device_model, network_model) + + if _use_linear_loss(F, device_model) + ll_devices = _devices_with_linear_loss(devices) + if !isempty(ll_devices) + _add_abs_value_decomposition_constraints!( + container, ll_devices, device_model, network_model, + DCLineCurrentFlowVariable, _vsc_shared_i_max, + ) + end + end + + add_constraint_dual!(container, sys, device_model) + add_feedforward_constraints!(container, device_model, devices) + return +end + +# AreaBalancePowerModel warning (consistent with other two-terminal formulations). +function construct_device!( + ::OptimizationContainer, + ::PSY.System, + ::ArgumentConstructStage, + ::DeviceModel{PSY.TwoTerminalVSCLine, <:AbstractTwoTerminalVSCFormulation}, + ::NetworkModel{AreaBalancePowerModel}, +) + @warn "AreaBalancePowerModel doesn't model individual line flows for PSY.TwoTerminalVSCLine. Arguments not built" + return +end + +function construct_device!( + ::OptimizationContainer, + ::PSY.System, + ::ModelConstructStage, + ::DeviceModel{PSY.TwoTerminalVSCLine, <:AbstractTwoTerminalVSCFormulation}, + ::NetworkModel{AreaBalancePowerModel}, +) + @warn "AreaBalancePowerModel doesn't model individual line flows for PSY.TwoTerminalVSCLine. Model not built" + return +end diff --git a/src/common_models/quadratic_converter_loss.jl b/src/common_models/quadratic_converter_loss.jl new file mode 100644 index 0000000..b2ad2eb --- /dev/null +++ b/src/common_models/quadratic_converter_loss.jl @@ -0,0 +1,124 @@ +# Shared helpers for quadratic / two-term converter losses +# loss(I) = a * I^2 + b * |I| + c +# Used by multi-terminal InterconnectingConverter formulations +# (Bin2QuadraticLossConverter, QuadraticLossConverter) and two-terminal +# HVDCTwoTerminalVSC formulations. + +######################################### +######## Loss-curve introspection ####### +######################################### + +_get_quadratic_term(loss_fn::PSY.QuadraticCurve) = PSY.get_quadratic_term(loss_fn) +_get_quadratic_term(loss_fn) = 0.0 + +# Whether the formulation wants the b*|I| linear-loss term (which requires +# decomposing the current into positive/negative parts with a direction binary). +_use_linear_loss(::Type{Bin2QuadraticLossConverter}, _) = true +_use_linear_loss(::Type{QuadraticLossConverter}, model) = + get_attribute(model, "use_linear_loss") +_use_linear_loss(::Type{HVDCTwoTerminalVSCBin2}, _) = true +_use_linear_loss(::Type{HVDCTwoTerminalVSC}, model) = + get_attribute(model, "use_linear_loss") + +# Per-device test: does this device have a nonzero linear loss term anywhere? +# Dispatched on device type because different PSY devices store loss curves on +# different fields. +_has_linear_loss(d::PSY.InterconnectingConverter) = + !iszero(PSY.get_proportional_term(PSY.get_loss_function(d))) +_has_linear_loss(d::PSY.TwoTerminalVSCLine) = + !iszero(PSY.get_proportional_term(PSY.get_converter_loss_from(d))) || + !iszero(PSY.get_proportional_term(PSY.get_converter_loss_to(d))) + +function _devices_with_linear_loss(devices) + return [d for d in devices if _has_linear_loss(d)] +end + +######################################### +######## Loss expression builder ######## +######################################### + +# Returns the JuMP expression a*i_sq + b*(i_pos + i_neg) + c +# for a single (device, time). The b*(i_pos+i_neg) term is included only when +# the formulation has opted into the linear-loss path AND b is nonzero for +# this specific device. +function _quadratic_converter_loss_expr( + a, b, c, i_sq_t, i_pos_t, i_neg_t; use_linear_loss::Bool, +) + loss = a * i_sq_t + c + if use_linear_loss && !iszero(b) + loss += b * (i_pos_t + i_neg_t) + end + return loss +end + +######################################### +####### Abs-value decomposition ######### +######################################### + +# Adds the three variables (PositiveCurrent, NegativeCurrent, CurrentDirection) +# that decompose a signed current variable into i = i^+ - i^- with a binary +# direction indicator. Called from the ArgumentConstructStage. +function _add_abs_value_decomposition_variables!( + container::OptimizationContainer, + devices, + ::DeviceModel{D, F}, +) where {D <: PSY.Device, F} + add_variables!(container, PositiveCurrent, devices, F) + add_variables!(container, NegativeCurrent, devices, F) + add_variables!(container, CurrentDirection, devices, F) + return +end + +# Adds the three constraints implementing i = i^+ - i^- with the big-M +# direction binary bounds. The CurrentAbsoluteValueConstraint container is +# created internally with three meta-tagged sub-containers ("", "pos_ub", +# "neg_ub"). Caller passes the parent current variable type and a function +# `d -> i_max` so device-specific bound lookups stay device-specific. +function _add_abs_value_decomposition_constraints!( + container::OptimizationContainer, + devices, + ::DeviceModel{D, F}, + ::NetworkModel{<:AbstractPowerModel}, + parent_var_type::Type{<:VariableType}, + i_max_getter::Function, +) where {D <: PSY.Device, F} + time_steps = get_time_steps(container) + names = [PSY.get_name(d) for d in devices] + jump_model = get_jump_model(container) + i_var = get_variable(container, parent_var_type, D) + i_pos_var = get_variable(container, PositiveCurrent, D) + i_neg_var = get_variable(container, NegativeCurrent, D) + i_dir_var = get_variable(container, CurrentDirection, D) + + abs_val_const = add_constraints_container!( + container, CurrentAbsoluteValueConstraint, D, names, time_steps, + ) + pos_ub_const = add_constraints_container!( + container, CurrentAbsoluteValueConstraint, D, names, time_steps; + meta = "pos_ub", + ) + neg_ub_const = add_constraints_container!( + container, CurrentAbsoluteValueConstraint, D, names, time_steps; + meta = "neg_ub", + ) + + for d in devices + name = PSY.get_name(d) + i_max = i_max_getter(d) + for t in time_steps + abs_val_const[name, t] = JuMP.@constraint( + jump_model, + i_var[name, t] == i_pos_var[name, t] - i_neg_var[name, t], + ) + pos_ub_const[name, t] = JuMP.@constraint( + jump_model, + i_pos_var[name, t] <= i_max * i_dir_var[name, t], + ) + neg_ub_const[name, t] = JuMP.@constraint( + jump_model, + i_neg_var[name, t] <= i_max * (1 - i_dir_var[name, t]), + ) + end + end + return +end diff --git a/src/core/constraints.jl b/src/core/constraints.jl index 2dc7a17..107a794 100644 --- a/src/core/constraints.jl +++ b/src/core/constraints.jl @@ -419,6 +419,39 @@ v_d^i = v_d^r - R_d I_d """ struct HVDCTransmissionDCLineConstraint <: ConstraintType end +""" +Per-terminal converter power-balance constraint for two-terminal VSC HVDC: + +```math +\\begin{aligned} +p_{ft} &= v_f \\cdot I - (a_f I^2 + b_f |I| + c_f) \\\\ +p_{tf} &= -v_t \\cdot I - (a_t I^2 + b_t |I| + c_t) +\\end{aligned} +``` +""" +struct HVDCVSCConverterPowerConstraint <: ConstraintType end + +""" +Cable Ohm's law for a two-terminal HVDC link with explicit DC resistance: + +```math +v_f - v_t = (1/g) \\cdot I +``` +""" +struct HVDCCableOhmsLawConstraint <: ConstraintType end + +""" +PQ capability constraint at each terminal of a two-terminal VSC HVDC, added only +on AC networks. For `HVDCTwoTerminalVSC` this is the exact circle: + +```math +p_k^2 + q_k^2 \\le (S_k^{\\max})^2 \\quad k \\in \\{f, t\\} +``` + +For `HVDCTwoTerminalVSCBin2` it is replaced by an inscribed polygon to stay MILP. +""" +struct HVDCVSCReactiveCapabilityConstraint <: ConstraintType end + abstract type PowerVariableLimitsConstraint <: ConstraintType end """ Struct to create the constraint to limit active power input expressions. @@ -577,19 +610,6 @@ struct DCLineCurrentConstraint <: ConstraintType end struct NodalBalanceCurrentConstraint <: ConstraintType end -""" -Struct to create the constraints that compute the converter DC power based on current and voltage. - -The specified constraints are formulated as: -```math -\\begin{align*} -& p_c = 0.5 * (γ^sq - v^sq - i^sq), \\quad \\forall t \\in \\{1,\\dots, T\\} \\\\ -& γ_c = v_c + i_c, \\quad \\forall t \\in \\{1,\\dots, T\\} \\\\ -\\end{align*} -``` -""" -struct ConverterPowerCalculationConstraint <: ConstraintType end - """ Struct to create the constraints that decide the balance of AC and DC power of the converter. @@ -603,66 +623,6 @@ The specified constraints are formulated as: """ struct ConverterLossConstraint <: ConstraintType end -""" -Struct to create the McCormick envelopes constraints that decide the bounds on the DC active power. - -The specified constraints are formulated as: -```math -\\begin{align*} -& p_c >= V^{min} i_c + v_c I^{min} - I^{min}V^{min}, \\quad \\forall t \\in \\{1,\\dots, T\\} \\\\ -& p_c >= V^{max} i_c + v_c I^{max} - I^{max}V^{max}, \\quad \\forall t \\in \\{1,\\dots, T\\} \\\\ -& p_c <= V^{max} i_c + v_c I^{min} - I^{min}V^{max}, \\quad \\forall t \\in \\{1,\\dots, T\\} \\\\ -& p_c <= V^{min} i_c + v_c I^{max} - I^{max}V^{min}, \\quad \\forall t \\in \\{1,\\dots, T\\} \\\\ -\\end{align*} -``` -""" -struct ConverterMcCormickEnvelopes <: ConstraintType end - -""" -Struct to create the Quadratic PWL interpolation constraints that decide square value of the voltage. -In this case x = voltage and y = squared_voltage. -The specified constraints are formulated as: -```math -\\begin{align*} -& x = x_0 + \\sum_{k=1}^K (x_{k} - x_{k-1}) \\delta_k, \\quad \\forall t \\in \\{1,\\dots, T\\} \\\\ -& y = y_0 + \\sum_{k=1}^K (x_{k} - x_{k-1}) \\delta_k, \\quad \\forall t \\in \\{1,\\dots, T\\} \\\\ -& z_k \\le \\delta_k, \\quad \\forall t \\in \\{1,\\dots, T\\}, \\forall k \\in \\{1,\\dots, K-1\\} \\\\ -& z_k \\ge \\delta_{k+1}, \\quad \\forall t \\in \\{1,\\dots, T\\}, \\forall k \\in \\{1,\\dots, K-1\\} \\\\ -\\end{align*} -``` -""" -struct InterpolationVoltageConstraints <: ConstraintType end - -""" -Struct to create the Quadratic PWL interpolation constraints that decide square value of the current. -In this case x = current and y = squared_current. -The specified constraints are formulated as: -```math -\\begin{align*} -& x = x_0 + \\sum_{k=1}^K (x_{k} - x_{k-1}) \\delta_k, \\quad \\forall t \\in \\{1,\\dots, T\\} \\\\ -& y = y_0 + \\sum_{k=1}^K (x_{k} - x_{k-1}) \\delta_k, \\quad \\forall t \\in \\{1,\\dots, T\\} \\\\ -& z_k \\le \\delta_k, \\quad \\forall t \\in \\{1,\\dots, T\\}, \\forall k \\in \\{1,\\dots, K-1\\} \\\\ -& z_k \\ge \\delta_{k+1}, \\quad \\forall t \\in \\{1,\\dots, T\\}, \\forall k \\in \\{1,\\dots, K-1\\} \\\\ -\\end{align*} -``` -""" -struct InterpolationCurrentConstraints <: ConstraintType end - -""" -Struct to create the Quadratic PWL interpolation constraints that decide square value of the bilinear variable γ. -In this case x = γ and y = squared_γ. -The specified constraints are formulated as: -```math -\\begin{align*} -& x = x_0 + \\sum_{k=1}^K (x_{k} - x_{k-1}) \\delta_k, \\quad \\forall t \\in \\{1,\\dots, T\\} \\\\ -& y = y_0 + \\sum_{k=1}^K (x_{k} - x_{k-1}) \\delta_k, \\quad \\forall t \\in \\{1,\\dots, T\\} \\\\ -& z_k \\le \\delta_k, \\quad \\forall t \\in \\{1,\\dots, T\\}, \\forall k \\in \\{1,\\dots, K-1\\} \\\\ -& z_k \\ge \\delta_{k+1}, \\quad \\forall t \\in \\{1,\\dots, T\\}, \\forall k \\in \\{1,\\dots, K-1\\} \\\\ -\\end{align*} -``` -""" -struct InterpolationBilinearConstraints <: ConstraintType end - """ Struct to create the constraints that set the absolute value for the current to use in losses through a lossy Interconnecting Power Converter. The specified constraint is formulated as: diff --git a/src/core/formulations.jl b/src/core/formulations.jl index 1159327..533f84e 100644 --- a/src/core/formulations.jl +++ b/src/core/formulations.jl @@ -184,8 +184,27 @@ Branch type to represent non-linear LCC (line commutated converter) model on two """ struct HVDCTwoTerminalLCC <: AbstractTwoTerminalDCLineFormulation end -# Not Implemented -# struct VoltageSourceDC <: AbstractTwoTerminalDCLineFormulation end +""" +Abstract supertype for two-terminal voltage-source converter (VSC) HVDC formulations. +Models per-terminal converters with quadratic / two-term losses +(``a I^2 + b |I| + c``), a shared signed cable current, an explicit DC-side +cable resistance (``v_f - v_t = (1/g) \\cdot I``), and (on AC networks) independent +reactive-power control bounded by per-terminal PQ capability. +""" +abstract type AbstractTwoTerminalVSCFormulation <: AbstractTwoTerminalDCLineFormulation end + +""" +Two-terminal VSC formulation that keeps the bilinear ``v \\cdot I`` and quadratic +``I^2`` terms exact. Requires an NLP-capable solver (e.g. Ipopt). +""" +struct HVDCTwoTerminalVSC <: AbstractTwoTerminalVSCFormulation end + +""" +Two-terminal VSC formulation that approximates the bilinear ``v \\cdot I`` and +quadratic ``I^2`` terms with SOS2-based piecewise-linear envelopes (the same +Bin2 scheme used by `Bin2QuadraticLossConverter`). Stays MILP. +""" +struct HVDCTwoTerminalVSCBin2 <: AbstractTwoTerminalVSCFormulation end ############################### AC/DC Converter Formulations ##################################### abstract type AbstractConverterFormulation <: AbstractDeviceFormulation end @@ -201,9 +220,27 @@ Linear Loss InterconnectingConverter Model struct LinearLossConverter <: AbstractConverterFormulation end """ -Quadratic Loss InterconnectingConverter Model +Abstract supertype for InterconnectingConverter formulations with quadratic losses. +""" +abstract type AbstractQuadraticLossConverter <: AbstractConverterFormulation end + +""" +Quadratic Loss InterconnectingConverter using the Bin2 separable bilinear approximation +(`v·i = ½((v+i)² − v² − i²)`) with a SOS2-based PWL approximation for x². +""" +struct Bin2QuadraticLossConverter <: AbstractQuadraticLossConverter end + +""" +Quadratic Loss InterconnectingConverter using exact bilinear (v·i) and quadratic (i²) +products. Requires an NLP-capable solver (e.g., Ipopt). + +Attributes (set on `DeviceModel`): +- `use_linear_loss`: Include the linear loss term `b·(i⁺ + i⁻)` (default false). The + linear term requires an absolute-value reformulation that introduces a binary + variable, so this is only solvable by MINLP solvers (e.g., Gurobi); pure NLP + solvers like Ipopt cannot solve the model with this option enabled. """ -struct QuadraticLossConverter <: AbstractConverterFormulation end +struct QuadraticLossConverter <: AbstractQuadraticLossConverter end ############################## HVDC Lines Formulations ################################## abstract type AbstractDCLineFormulation <: AbstractBranchFormulation end diff --git a/src/core/variables.jl b/src/core/variables.jl index 660ef5e..44ab5bb 100644 --- a/src/core/variables.jl +++ b/src/core/variables.jl @@ -150,12 +150,6 @@ Docs abbreviation: ``i_l^{dc}`` """ struct DCLineCurrent <: VariableType end -""" -Struct to dispatch the creation of Squared Voltage Variables for DC formulations -Docs abbreviation: ``v^{sq,dc}`` -""" -struct SquaredDCVoltage <: VariableType end - """ Struct to dispatch the creation of DC Converter Current Variables for DC formulations Docs abbreviation: ``i_c^{dc}`` @@ -163,95 +157,25 @@ Docs abbreviation: ``i_c^{dc}`` struct ConverterCurrent <: VariableType end """ -Struct to dispatch the creation of DC Converter Power Variables for DC formulations -Docs abbreviation: ``p_c^{dc}`` -""" -struct ConverterDCPower <: VariableType end - -""" -Struct to dispatch the creation of Squared DC Converter Current Variables for DC formulations -Docs abbreviation: ``i_c^{sq,dc}`` -""" -struct SquaredConverterCurrent <: VariableType end - -""" -Struct to dispatch the creation of DC Converter Positive Term Current Variables for DC formulations -Docs abbreviation: ``i_c^{+,dc}`` -""" -struct ConverterPositiveCurrent <: VariableType end - -""" -Struct to dispatch the creation of DC Converter Negative Term Current Variables for DC formulations -Docs abbreviation: ``i_c^{-,dc}`` -""" -struct ConverterNegativeCurrent <: VariableType end - -""" -Struct to dispatch the creation of DC Converter Binary for Absolute Value Current Variables for DC formulations -Docs abbreviation: `\\nu_c`` -""" -struct ConverterCurrentDirection <: VariableType end - -""" -Struct to dispatch the creation of Binary Variable for Converter Power Direction -Docs abbreviation: ``\\kappa_c^{dc}`` -""" -struct ConverterPowerDirection <: VariableType end - -""" -Struct to dispatch the creation of Auxiliary Variable for Converter Bilinear term: v * i -Docs abbreviation: ``\\gamma_c^{dc}`` +Positive part of an absolute-value current decomposition (``i = i^+ - i^-``). +Used by both two-terminal HVDC links (cable current) and interconnecting converters. +Docs abbreviation: ``i^{+,dc}`` """ -struct AuxBilinearConverterVariable <: VariableType end +struct PositiveCurrent <: VariableType end """ -Struct to dispatch the creation of Auxiliary Variable for Squared Converter Bilinear term: v * i - -Docs abbreviation: ``\\gamma_c^{sq,dc}`` -""" -struct AuxBilinearSquaredConverterVariable <: VariableType end - +Negative part of an absolute-value current decomposition (``i = i^+ - i^-``). +Used by both two-terminal HVDC links (cable current) and interconnecting converters. +Docs abbreviation: ``i^{-,dc}`` """ -Struct to dispatch the creation of Continuous Interpolation Variable for Squared Converter Voltage +struct NegativeCurrent <: VariableType end -Docs abbreviation: ``\\delta_c^{v}`` """ -struct InterpolationSquaredVoltageVariable <: InterpolationVariableType end - +Binary indicator selecting which sign the current takes in the absolute-value +decomposition (``i^+`` active when 1, ``i^-`` active when 0). +Docs abbreviation: ``\\nu`` """ -Struct to dispatch the creation of Binary Interpolation Variable for Squared Converter Voltage - -Docs abbreviation: ``z_c^{v}`` -""" -struct InterpolationBinarySquaredVoltageVariable <: BinaryInterpolationVariableType end - -""" -Struct to dispatch the creation of Continuous Interpolation Variable for Squared Converter Current - -Docs abbreviation: ``\\delta_c^{i}`` -""" -struct InterpolationSquaredCurrentVariable <: InterpolationVariableType end - -""" -Struct to dispatch the creation of Binary Interpolation Variable for Squared Converter Current - -Docs abbreviation: ``z_c^{i}`` -""" -struct InterpolationBinarySquaredCurrentVariable <: BinaryInterpolationVariableType end - -""" -Struct to dispatch the creation of Continuous Interpolation Variable for Squared Converter AuxVar - -Docs abbreviation: ``\\delta_c^{\\gamma}`` -""" -struct InterpolationSquaredBilinearVariable <: InterpolationVariableType end - -""" -Struct to dispatch the creation of Binary Interpolation Variable for Squared Converter AuxVar - -Docs abbreviation: ``z_c^{\\gamma}`` -""" -struct InterpolationBinarySquaredBilinearVariable <: BinaryInterpolationVariableType end +struct CurrentDirection <: VariableType end ######################################################### ######################################################### @@ -463,6 +387,34 @@ Docs abbreviation: ``z`` """ struct HVDCPiecewiseBinaryLossVariable <: SparseVariableType end +""" +DC-side voltage at the from-terminal of a two-terminal HVDC link. +Used by `HVDCTwoTerminalVSC` formulations. +Docs abbreviation: ``v_f^{dc}`` +""" +struct HVDCFromDCVoltage <: VariableType end + +""" +DC-side voltage at the to-terminal of a two-terminal HVDC link. +Used by `HVDCTwoTerminalVSC` formulations. +Docs abbreviation: ``v_t^{dc}`` +""" +struct HVDCToDCVoltage <: VariableType end + +""" +Reactive power injected at the from-terminal AC bus by a two-terminal HVDC link. +Added only when the formulation runs on an AC network model. +Docs abbreviation: ``q_f`` +""" +struct HVDCReactivePowerFromVariable <: VariableType end + +""" +Reactive power injected at the to-terminal AC bus by a two-terminal HVDC link. +Added only when the formulation runs on an AC network model. +Docs abbreviation: ``q_t`` +""" +struct HVDCReactivePowerToVariable <: VariableType end + """ Struct to dispatch the creation of Interface Flow Slack Up variables diff --git a/src/mt_hvdc_models/HVDCsystems.jl b/src/mt_hvdc_models/HVDCsystems.jl index cfa40e1..3d197d8 100644 --- a/src/mt_hvdc_models/HVDCsystems.jl +++ b/src/mt_hvdc_models/HVDCsystems.jl @@ -119,63 +119,38 @@ end ############################################ ## Binaries ### -get_variable_binary(::Type{ConverterDCPower}, ::Type{PSY.InterconnectingConverter}, ::Type{<:AbstractConverterFormulation}) = false -get_variable_binary(::Type{ConverterPowerDirection}, ::Type{PSY.InterconnectingConverter}, ::Type{<:AbstractConverterFormulation}) = true get_variable_binary(::Type{ConverterCurrent}, ::Type{PSY.InterconnectingConverter}, ::Type{<:AbstractConverterFormulation}) = false -get_variable_binary(::Type{ConverterPositiveCurrent}, ::Type{PSY.InterconnectingConverter}, ::Type{<:AbstractConverterFormulation}) = false -get_variable_binary(::Type{ConverterNegativeCurrent}, ::Type{PSY.InterconnectingConverter}, ::Type{<:AbstractConverterFormulation}) = false -get_variable_binary(::Type{ConverterCurrentDirection}, ::Type{PSY.InterconnectingConverter}, ::Type{<:AbstractConverterFormulation}) = true -get_variable_binary(::Type{SquaredConverterCurrent}, ::Type{PSY.InterconnectingConverter}, ::Type{<:AbstractConverterFormulation}) = false -get_variable_binary(::Type{SquaredDCVoltage}, ::Type{PSY.InterconnectingConverter}, ::Type{<:AbstractConverterFormulation}) = false -get_variable_binary(::Type{AuxBilinearConverterVariable}, ::Type{PSY.InterconnectingConverter}, ::Type{<:AbstractConverterFormulation}) = false -get_variable_binary(::Type{AuxBilinearSquaredConverterVariable}, ::Type{PSY.InterconnectingConverter}, ::Type{<:AbstractConverterFormulation}) = false -function get_variable_binary( - ::Type{W}, - ::Type{PSY.InterconnectingConverter}, - ::Type{<:AbstractConverterFormulation} -) where W <: InterpolationVariableType - return false -end -function get_variable_binary( - ::Type{W}, - ::Type{PSY.InterconnectingConverter}, - ::Type{<:AbstractConverterFormulation} -) where W <: BinaryInterpolationVariableType - return true -end - +get_variable_binary(::Type{PositiveCurrent}, ::Type{PSY.InterconnectingConverter}, ::Type{<:AbstractConverterFormulation}) = false +get_variable_binary(::Type{NegativeCurrent}, ::Type{PSY.InterconnectingConverter}, ::Type{<:AbstractConverterFormulation}) = false +get_variable_binary(::Type{CurrentDirection}, ::Type{PSY.InterconnectingConverter}, ::Type{<:AbstractConverterFormulation}) = true ### Warm Start ### get_variable_warm_start_value(::Type{ConverterCurrent}, d::PSY.InterconnectingConverter, ::Type{<:AbstractConverterFormulation}) = PSY.get_dc_current(d) ### Lower Bounds ### -get_variable_lower_bound(::Type{ConverterDCPower}, d::PSY.InterconnectingConverter, ::Type{<:AbstractConverterFormulation}) = PSY.get_active_power_limits(d).min get_variable_lower_bound(::Type{ConverterCurrent}, d::PSY.InterconnectingConverter, ::Type{<:AbstractConverterFormulation}) = -PSY.get_max_dc_current(d) -get_variable_lower_bound(::Type{SquaredConverterCurrent}, d::PSY.InterconnectingConverter, ::Type{<:AbstractConverterFormulation}) = 0.0 -get_variable_lower_bound(::Type{SquaredDCVoltage}, d::PSY.InterconnectingConverter, ::Type{<:AbstractConverterFormulation}) = PSY.get_voltage_limits(d.dc_bus).min^2 -get_variable_lower_bound(::Type{<:InterpolationVariableType}, d::PSY.InterconnectingConverter, ::Type{<:AbstractConverterFormulation}) = 0.0 -get_variable_lower_bound(::Type{ConverterPositiveCurrent}, d::PSY.InterconnectingConverter,::Type{<:AbstractConverterFormulation}) = 0.0 -get_variable_lower_bound(::Type{ConverterNegativeCurrent}, d::PSY.InterconnectingConverter,::Type{<:AbstractConverterFormulation}) = 0.0 +get_variable_lower_bound(::Type{PositiveCurrent}, d::PSY.InterconnectingConverter,::Type{<:AbstractConverterFormulation}) = 0.0 +get_variable_lower_bound(::Type{NegativeCurrent}, d::PSY.InterconnectingConverter,::Type{<:AbstractConverterFormulation}) = 0.0 ### Upper Bounds ### -get_variable_upper_bound(::Type{ConverterDCPower}, d::PSY.InterconnectingConverter, ::Type{<:AbstractConverterFormulation}) = PSY.get_active_power_limits(d).max get_variable_upper_bound(::Type{ConverterCurrent}, d::PSY.InterconnectingConverter, ::Type{<:AbstractConverterFormulation}) = PSY.get_max_dc_current(d) -get_variable_upper_bound(::Type{SquaredConverterCurrent}, d::PSY.InterconnectingConverter, ::Type{<:AbstractConverterFormulation}) = PSY.get_max_dc_current(d)^2 -get_variable_upper_bound(::Type{SquaredDCVoltage}, d::PSY.InterconnectingConverter, ::Type{<:AbstractConverterFormulation}) = PSY.get_voltage_limits(d.dc_bus).max^2 -get_variable_upper_bound(::Type{<:InterpolationVariableType}, d::PSY.InterconnectingConverter, ::Type{<:AbstractConverterFormulation}) = 1.0 -get_variable_upper_bound(::Type{ConverterPositiveCurrent}, d::PSY.InterconnectingConverter,::Type{<:AbstractConverterFormulation}) = PSY.get_max_dc_current(d) -get_variable_upper_bound(::Type{ConverterNegativeCurrent}, d::PSY.InterconnectingConverter,::Type{<:AbstractConverterFormulation}) = PSY.get_max_dc_current(d) +get_variable_upper_bound(::Type{PositiveCurrent}, d::PSY.InterconnectingConverter,::Type{<:AbstractConverterFormulation}) = PSY.get_max_dc_current(d) +get_variable_upper_bound(::Type{NegativeCurrent}, d::PSY.InterconnectingConverter,::Type{<:AbstractConverterFormulation}) = PSY.get_max_dc_current(d) +function get_default_attributes( + ::Type{PSY.InterconnectingConverter}, + ::Type{Bin2QuadraticLossConverter}, +) + return Dict{String, Any}() +end + function get_default_attributes( ::Type{PSY.InterconnectingConverter}, ::Type{QuadraticLossConverter}, ) return Dict{String, Any}( - "voltage_segments" => 3, - "current_segments" => 6, - "bilinear_segments" => 10, - "use_linear_loss" => true, + "use_linear_loss" => false, ) end @@ -422,7 +397,7 @@ function add_to_expression!( T <: ActivePowerBalance, U <: ActivePowerVariable, V <: PSY.InterconnectingConverter, - W <: QuadraticLossConverter, + W <: AbstractQuadraticLossConverter, } variable = get_variable(container, U, V) sys_expr = get_expression(container, T, PSY.System) @@ -452,7 +427,7 @@ function add_to_expression!( T <: DCCurrentBalance, U <: ConverterCurrent, V <: PSY.InterconnectingConverter, - W <: QuadraticLossConverter, + W <: AbstractQuadraticLossConverter, } variable = get_variable(container, U, V) expression_dc = get_expression(container, T, PSY.DCBus) @@ -564,259 +539,51 @@ function add_constraints!( end ############## Converters ################## -function add_constraints!( - container::OptimizationContainer, - ::Type{ConverterPowerCalculationConstraint}, - devices::IS.FlattenIteratorWrapper{U}, - model::DeviceModel{U, V}, - network_model::NetworkModel{X}, -) where { - U <: PSY.InterconnectingConverter, - V <: QuadraticLossConverter, - X <: AbstractActivePowerModel, -} - time_steps = get_time_steps(container) - varcurrent = get_variable(container, ConverterCurrent, U) - var_dcvoltage = get_variable(container, DCVoltage, PSY.DCBus) - var_sq_current = get_variable(container, SquaredConverterCurrent, U) - var_sq_voltage = get_variable(container, SquaredDCVoltage, U) - var_bilinear = get_variable(container, AuxBilinearConverterVariable, U) - var_sq_bilinear = get_variable(container, AuxBilinearSquaredConverterVariable, U) - var_dc_power = get_variable(container, ConverterDCPower, U) - ipc_names = axes(varcurrent, 1) - constraint = - add_constraints_container!(container, ConverterPowerCalculationConstraint, - U, - ipc_names, - time_steps, - ) - constraint_aux = - add_constraints_container!(container, ConverterPowerCalculationConstraint, - U, - ipc_names, - time_steps; - meta = "aux", - ) - - for device in devices - name = PSY.get_name(device) - dc_bus_name = PSY.get_name(PSY.get_dc_bus(device)) - for t in time_steps - # p_dc = v_dc * i_dc = 0.5 * (bilinear - v_dc^2 - i_dc^2) - constraint[name, t] = JuMP.@constraint( - get_jump_model(container), - var_dc_power[name, t] == - 0.5 * ( - var_sq_bilinear[name, t] - var_sq_voltage[name, t] - - var_sq_current[name, t] - ) - ) - constraint_aux[name, t] = JuMP.@constraint( - get_jump_model(container), - var_bilinear[name, t] == - var_dcvoltage[dc_bus_name, t] + varcurrent[name, t] - ) - end - end - return -end - -function add_constraints!( - container::OptimizationContainer, - ::Type{ConverterMcCormickEnvelopes}, - devices::IS.FlattenIteratorWrapper{U}, - model::DeviceModel{U, V}, - network_model::NetworkModel{X}, -) where { - U <: PSY.InterconnectingConverter, - V <: QuadraticLossConverter, - X <: AbstractActivePowerModel, -} - time_steps = get_time_steps(container) - varcurrent = get_variable(container, ConverterCurrent, U) - var_dcvoltage = get_variable(container, DCVoltage, PSY.DCBus) - var_dc_power = get_variable(container, ConverterDCPower, U) - ipc_names = axes(varcurrent, 1) - constraint1_under = - add_constraints_container!(container, ConverterMcCormickEnvelopes, - U, - ipc_names, - time_steps; - meta = "under_1", - ) - constraint2_under = - add_constraints_container!(container, ConverterMcCormickEnvelopes, - U, - ipc_names, - time_steps; - meta = "under_2", - ) - constraint1_over = - add_constraints_container!(container, ConverterMcCormickEnvelopes, - U, - ipc_names, - time_steps; - meta = "over_1", - ) - constraint2_over = - add_constraints_container!(container, ConverterMcCormickEnvelopes, - U, - ipc_names, - time_steps; - meta = "over_2", - ) - - for device in devices - name = PSY.get_name(device) - dc_bus = PSY.get_dc_bus(device) - dc_bus_name = PSY.get_name(dc_bus) - V_min, V_max = PSY.get_voltage_limits(dc_bus) - I_max = PSY.get_max_dc_current(device) - I_min = -I_max - for t in time_steps - constraint1_under[name, t] = JuMP.@constraint( - get_jump_model(container), - var_dc_power[name, t] >= - V_min * varcurrent[name, t] + var_dcvoltage[dc_bus_name, t] * I_min - - I_min * V_min - ) - constraint2_under[name, t] = JuMP.@constraint( - get_jump_model(container), - var_dc_power[name, t] >= - V_max * varcurrent[name, t] + var_dcvoltage[dc_bus_name, t] * I_max - - I_max * V_max - ) - constraint1_over[name, t] = JuMP.@constraint( - get_jump_model(container), - var_dc_power[name, t] <= - V_max * varcurrent[name, t] + var_dcvoltage[dc_bus_name, t] * I_min - - I_min * V_max - ) - constraint2_over[name, t] = JuMP.@constraint( - get_jump_model(container), - var_dc_power[name, t] <= - V_min * varcurrent[name, t] + var_dcvoltage[dc_bus_name, t] * I_max - - I_max * V_min - ) - end - end - return -end function add_constraints!( container::OptimizationContainer, ::Type{ConverterLossConstraint}, devices::IS.FlattenIteratorWrapper{U}, model::DeviceModel{U, V}, - network_model::NetworkModel{X}, + ::NetworkModel{X}, ) where { U <: PSY.InterconnectingConverter, - V <: QuadraticLossConverter, + V <: AbstractQuadraticLossConverter, X <: AbstractActivePowerModel, } time_steps = get_time_steps(container) - var_sq_current = get_variable(container, SquaredConverterCurrent, U) - var_ac_power = get_variable(container, ActivePowerVariable, U) - var_dc_power = get_variable(container, ConverterDCPower, U) - ipc_names = axes(var_sq_current, 1) - constraint = - add_constraints_container!(container, ConverterLossConstraint, - U, - ipc_names, - time_steps, - ) - - use_linear_loss = get_attribute(model, "use_linear_loss") + P_ac_var = get_variable(container, ActivePowerVariable, U) + vi_expr = get_expression(container, IOM.BilinearProductExpression, U, "vi") + i_sq_expr = get_expression(container, IOM.QuadraticExpression, U, "i_sq") + use_linear_loss = + _use_linear_loss(V, model) && !isempty(_devices_with_linear_loss(devices)) if use_linear_loss - pos_current = get_variable(container, ConverterPositiveCurrent, U) - neg_current = get_variable(container, ConverterNegativeCurrent, U) + i_pos_var = get_variable(container, PositiveCurrent, U) + i_neg_var = get_variable(container, NegativeCurrent, U) end + ipc_names = [PSY.get_name(d) for d in devices] + loss_const = add_constraints_container!( + container, ConverterLossConstraint, U, ipc_names, time_steps, + ) + + jump_model = get_jump_model(container) for device in devices name = PSY.get_name(device) loss_function = PSY.get_loss_function(device) - if isa(loss_function, PSY.QuadraticCurve) - a = PSY.get_quadratic_term(loss_function) - b = PSY.get_proportional_term(loss_function) - c = PSY.get_constant_term(loss_function) - else - a = 0.0 - b = PSY.get_proportional_term(loss_function) - c = PSY.get_constant_term(loss_function) - end + a = _get_quadratic_term(loss_function) + b = PSY.get_proportional_term(loss_function) + c = PSY.get_constant_term(loss_function) for t in time_steps - if use_linear_loss - loss = - a * var_sq_current[name, t] + - b * (pos_current[name, t] + neg_current[name, t]) + c - else - loss = a * var_sq_current[name, t] + c - end - constraint[name, t] = JuMP.@constraint( - get_jump_model(container), - var_ac_power[name, t] == var_dc_power[name, t] - loss - ) - end - end - return -end - -function add_constraints!( - container::OptimizationContainer, - ::Type{T}, - devices::IS.FlattenIteratorWrapper{U}, - ::DeviceModel{U, V}, - ::NetworkModel{<:AbstractPowerModel}, -) where { - T <: CurrentAbsoluteValueConstraint, - U <: PSY.InterconnectingConverter, - V <: QuadraticLossConverter, -} - time_steps = get_time_steps(container) - names = [PSY.get_name(d) for d in devices] - JuMPmodel = get_jump_model(container) - # current vars # - current_var = get_variable(container, ConverterCurrent, U) # From direction - current_var_pos = get_variable(container, ConverterPositiveCurrent, U) # From direction - current_var_neg = get_variable(container, ConverterNegativeCurrent, U) # From direction - current_dir = get_variable(container, ConverterCurrentDirection, U) - - constraint = - add_constraints_container!(container, CurrentAbsoluteValueConstraint, - U, - names, - time_steps, - ) - constraint_pos_ub = - add_constraints_container!(container, CurrentAbsoluteValueConstraint, - U, - names, - time_steps; - meta = "pos_ub", - ) - constraint_neg_ub = - add_constraints_container!(container, CurrentAbsoluteValueConstraint, - U, - names, - time_steps; - meta = "neg_ub", - ) - - for d in devices - name = PSY.get_name(d) - I_max = PSY.get_max_dc_current(d) - for t in time_steps - constraint[name, t] = JuMP.@constraint( - JuMPmodel, - current_var[name, t] == current_var_pos[name, t] - current_var_neg[name, t] - ) - constraint_pos_ub[name, t] = JuMP.@constraint( - JuMPmodel, - current_var_pos[name, t] <= I_max * current_dir[name, t] + i_pos_t = use_linear_loss ? i_pos_var[name, t] : nothing + i_neg_t = use_linear_loss ? i_neg_var[name, t] : nothing + loss = _quadratic_converter_loss_expr( + a, b, c, i_sq_expr[name, t], i_pos_t, i_neg_t; + use_linear_loss = use_linear_loss, ) - constraint_neg_ub[name, t] = JuMP.@constraint( - JuMPmodel, - current_var_neg[name, t] <= I_max * (1 - current_dir[name, t]) + loss_const[name, t] = JuMP.@constraint( + jump_model, + P_ac_var[name, t] == vi_expr[name, t] - loss, ) end end @@ -826,115 +593,18 @@ end function add_constraints!( container::OptimizationContainer, ::Type{T}, - devices::IS.FlattenIteratorWrapper{U}, - model::DeviceModel{U, V}, - ::NetworkModel{<:AbstractPowerModel}, -) where { - T <: InterpolationVoltageConstraints, - U <: PSY.InterconnectingConverter, - V <: QuadraticLossConverter, -} - dic_var_bkpts = Dict{String, Vector{Float64}}() - dic_function_bkpts = Dict{String, Vector{Float64}}() - num_segments = get_attribute(model, "voltage_segments") - for d in devices - name = PSY.get_name(d) - vmin, vmax = PSY.get_voltage_limits(d.dc_bus) - var_bkpts, function_bkpts = - _get_breakpoints_for_pwl_function(vmin, vmax, x -> x^2; num_segments) - dic_var_bkpts[name] = var_bkpts - dic_function_bkpts[name] = function_bkpts - end - - _add_generic_incremental_interpolation_constraint!( - container, - DCVoltage, - SquaredDCVoltage, - InterpolationSquaredVoltageVariable, - InterpolationBinarySquaredVoltageVariable, - InterpolationVoltageConstraints, - devices, - dic_var_bkpts, - dic_function_bkpts, - ) - return -end - -function add_constraints!( - container::OptimizationContainer, - ::Type{T}, - devices::IS.FlattenIteratorWrapper{U}, - model::DeviceModel{U, V}, - ::NetworkModel{<:AbstractPowerModel}, -) where { - T <: InterpolationCurrentConstraints, - U <: PSY.InterconnectingConverter, - V <: QuadraticLossConverter, -} - dic_var_bkpts = Dict{String, Vector{Float64}}() - dic_function_bkpts = Dict{String, Vector{Float64}}() - num_segments = get_attribute(model, "current_segments") - for d in devices - name = PSY.get_name(d) - Imax = PSY.get_max_dc_current(d) - Imin = -Imax - var_bkpts, function_bkpts = - _get_breakpoints_for_pwl_function(Imin, Imax, x -> x^2; num_segments) - dic_var_bkpts[name] = var_bkpts - dic_function_bkpts[name] = function_bkpts - end - - _add_generic_incremental_interpolation_constraint!( - container, - ConverterCurrent, - SquaredConverterCurrent, - InterpolationSquaredCurrentVariable, - InterpolationBinarySquaredCurrentVariable, - InterpolationCurrentConstraints, - devices, - dic_var_bkpts, - dic_function_bkpts, - ) - return -end - -function add_constraints!( - container::OptimizationContainer, - ::Type{T}, - devices::IS.FlattenIteratorWrapper{U}, + devices::W, model::DeviceModel{U, V}, - ::NetworkModel{<:AbstractPowerModel}, + network_model::NetworkModel{<:AbstractPowerModel}, ) where { - T <: InterpolationBilinearConstraints, + T <: CurrentAbsoluteValueConstraint, U <: PSY.InterconnectingConverter, - V <: QuadraticLossConverter, + V <: AbstractQuadraticLossConverter, + W <: Union{Vector{U}, IS.FlattenIteratorWrapper{U}}, } - dic_var_bkpts = Dict{String, Vector{Float64}}() - dic_function_bkpts = Dict{String, Vector{Float64}}() - num_segments = get_attribute(model, "bilinear_segments") - for d in devices - name = PSY.get_name(d) - vmin, vmax = PSY.get_voltage_limits(d.dc_bus) - Imax = PSY.get_max_dc_current(d) - Imin = -Imax - γ_min = vmin * Imin - γ_max = vmax * Imax - var_bkpts, function_bkpts = - _get_breakpoints_for_pwl_function(γ_min, γ_max, x -> x^2; num_segments) - dic_var_bkpts[name] = var_bkpts - dic_function_bkpts[name] = function_bkpts - end - - _add_generic_incremental_interpolation_constraint!( - container, - AuxBilinearConverterVariable, - AuxBilinearSquaredConverterVariable, - InterpolationSquaredBilinearVariable, - InterpolationBinarySquaredBilinearVariable, - InterpolationBilinearConstraints, - devices, - dic_var_bkpts, - dic_function_bkpts, + _add_abs_value_decomposition_constraints!( + container, devices, model, network_model, + ConverterCurrent, PSY.get_max_dc_current, ) return end diff --git a/src/mt_hvdc_models/hvdcsystems_constructor.jl b/src/mt_hvdc_models/hvdcsystems_constructor.jl index 138eada..249d856 100644 --- a/src/mt_hvdc_models/hvdcsystems_constructor.jl +++ b/src/mt_hvdc_models/hvdcsystems_constructor.jl @@ -48,183 +48,126 @@ function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, - model::DeviceModel{PSY.InterconnectingConverter, QuadraticLossConverter}, + model::DeviceModel{PSY.InterconnectingConverter, T}, network_model::NetworkModel{<:AbstractActivePowerModel}, -) - devices = get_available_components( - model, - sys, - ) - ##################### - ##### Variables ##### - ##################### - - # Add Power Variable - add_variables!(container, ActivePowerVariable, devices, QuadraticLossConverter) # p_c^{ac} - add_variables!(container, ConverterDCPower, devices, QuadraticLossConverter) # p_c - # Add Current Variables: i, i+, i- - add_variables!(container, ConverterCurrent, devices, QuadraticLossConverter) # i - add_variables!(container, SquaredConverterCurrent, devices, QuadraticLossConverter) # i^sq - use_linear_loss = get_attribute(model, "use_linear_loss") - if use_linear_loss - add_variables!( - container, - ConverterPositiveCurrent, - devices, - QuadraticLossConverter, - ) # i^+ - add_variables!( - container, - ConverterNegativeCurrent, - devices, - QuadraticLossConverter, - ) # i^- - add_variables!( - container, - ConverterCurrentDirection, - devices, - QuadraticLossConverter, - ) # ν +) where {T <: AbstractQuadraticLossConverter} + devices = get_available_components(model, sys) + add_variables!(container, ActivePowerVariable, devices, T) + add_variables!(container, ConverterCurrent, devices, T) + if _use_linear_loss(T, model) + ll_devices = _devices_with_linear_loss(devices) + if isempty(ll_devices) + @warn "use_linear_loss is enabled but every InterconnectingConverter has a zero proportional loss term; no linear-loss variables/constraints will be added." + else + add_variables!(container, PositiveCurrent, ll_devices, T) + add_variables!(container, NegativeCurrent, ll_devices, T) + add_variables!(container, CurrentDirection, ll_devices, T) + end end - # Add Voltage Variables: v^sq - add_variables!(container, SquaredDCVoltage, devices, QuadraticLossConverter) - # Add Bilinear Variables: γ, γ^{sq} - add_variables!( - container, - AuxBilinearConverterVariable, - devices, - QuadraticLossConverter, - ) # γ - add_variables!( - container, - AuxBilinearSquaredConverterVariable, - devices, - QuadraticLossConverter, - ) # γ^{sq} - - #### Add Interpolation Variables #### - - v_segments = get_attribute(model, "voltage_segments") - i_segments = get_attribute(model, "current_segments") - γ_segments = get_attribute(model, "bilinear_segments") - - vars_vector = [ - # Voltage v # - (InterpolationSquaredVoltageVariable, v_segments), # δ^v - (InterpolationBinarySquaredVoltageVariable, v_segments), # z^v - # Current i # - (InterpolationSquaredCurrentVariable, i_segments), # δ^i - (InterpolationBinarySquaredCurrentVariable, i_segments), # z^i - # Bilinear γ # - (InterpolationSquaredBilinearVariable, γ_segments), # δ^γ - (InterpolationBinarySquaredBilinearVariable, γ_segments), # z^γ - ] - - for (T, len_segments) in vars_vector - add_sparse_pwl_interpolation_variables!(container, T, - devices, - model, - len_segments, - ) - end - - ##################### - #### Expressions #### - ##################### add_to_expression!( - container, - ActivePowerBalance, - ActivePowerVariable, - devices, - model, - network_model, + container, ActivePowerBalance, ActivePowerVariable, + devices, model, network_model, ) add_to_expression!( - container, - DCCurrentBalance, - ConverterCurrent, - devices, - model, - network_model, + container, DCCurrentBalance, ConverterCurrent, + devices, model, network_model, ) add_feedforward_arguments!(container, model, devices) return end +function _voltage_expr_per_converter( + container::OptimizationContainer, + devices, + ipc_names::Vector{String}, + time_steps, +) + v_var = get_variable(container, DCVoltage, PSY.DCBus) + bus_names = [PSY.get_name(PSY.get_dc_bus(d)) for d in devices] + return JuMP.Containers.DenseAxisArray( + [v_var[b, t] for b in bus_names, t in time_steps], + ipc_names, time_steps, + ) +end + +function _converter_vi_bounds(devices) + n = length(devices) + v_bounds = Vector{IOM.MinMax}(undef, n) + i_bounds = Vector{IOM.MinMax}(undef, n) + for (k, d) in enumerate(devices) + v_min, v_max = PSY.get_voltage_limits(PSY.get_dc_bus(d)) + i_max = PSY.get_max_dc_current(d) + v_bounds[k] = IOM.MinMax((min = v_min, max = v_max)) + i_bounds[k] = IOM.MinMax((min = -i_max, max = i_max)) + end + return v_bounds, i_bounds +end + +_quad_config(::Type{Bin2QuadraticLossConverter}) = + IOM.SolverSOS2QuadConfig(DEFAULT_INTERPOLATION_LENGTH) +_quad_config(::Type{QuadraticLossConverter}) = IOM.NoQuadApproxConfig() +_bilinear_config(::Type{Bin2QuadraticLossConverter}) = + IOM.Bin2Config(IOM.SolverSOS2QuadConfig(DEFAULT_INTERPOLATION_LENGTH)) +_bilinear_config(::Type{QuadraticLossConverter}) = IOM.NoBilinearApproxConfig() + function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, - model::DeviceModel{PSY.InterconnectingConverter, QuadraticLossConverter}, + model::DeviceModel{PSY.InterconnectingConverter, T}, network_model::NetworkModel{<:AbstractActivePowerModel}, -) - devices = get_available_components( - model, - sys, - ) - - add_constraints!( - container, - ConverterPowerCalculationConstraint, - devices, - model, - network_model, - ) - add_constraints!( - container, - ConverterMcCormickEnvelopes, - devices, - model, - network_model, - ) - add_constraints!( - container, - ConverterLossConstraint, - devices, - model, - network_model, - ) - use_linear_loss = get_attribute(model, "use_linear_loss") - if use_linear_loss - add_constraints!( - container, - CurrentAbsoluteValueConstraint, - devices, - model, - network_model, - ) +) where {T <: AbstractQuadraticLossConverter} + devices = get_available_components(model, sys) + time_steps = get_time_steps(container) + ipc_names = [PSY.get_name(d) for d in devices] + v_bounds, i_bounds = _converter_vi_bounds(devices) + v_expr = _voltage_expr_per_converter(container, devices, ipc_names, time_steps) + i_var = get_variable(container, ConverterCurrent, PSY.InterconnectingConverter) + + quad_cfg, bilin_cfg = _quad_config(T), _bilinear_config(T) + v_sq_expr = IOM._add_quadratic_approx!( + quad_cfg, + container, PSY.InterconnectingConverter, + ipc_names, time_steps, + v_expr, v_bounds, + "v_sq", + ) + i_sq_expr = IOM._add_quadratic_approx!( + quad_cfg, + container, PSY.InterconnectingConverter, + ipc_names, time_steps, + i_var, i_bounds, + "i_sq", + ) + IOM._add_bilinear_approx!( + bilin_cfg, + container, PSY.InterconnectingConverter, + ipc_names, time_steps, + v_sq_expr, i_sq_expr, + v_expr, i_var, + v_bounds, i_bounds, + "vi", + ) + + add_constraints!(container, ConverterLossConstraint, devices, model, network_model) + if _use_linear_loss(T, model) + ll_devices = _devices_with_linear_loss(devices) + if !isempty(ll_devices) + add_constraints!( + container, + CurrentAbsoluteValueConstraint, + ll_devices, + model, + network_model, + ) + end end - add_constraints!( - container, - InterpolationVoltageConstraints, - devices, - model, - network_model, - ) - add_constraints!( - container, - InterpolationCurrentConstraints, - devices, - model, - network_model, - ) - add_constraints!( - container, - InterpolationBilinearConstraints, - devices, - model, - network_model, - ) add_feedforward_constraints!(container, model, devices) add_to_objective_function!( - container, - devices, - model, - get_network_formulation(network_model), + container, devices, model, get_network_formulation(network_model), ) - #add_constraint_dual!(container, sys, model) return end diff --git a/src/network_models/pm_translator.jl b/src/network_models/pm_translator.jl index 09f999e..46e8827 100644 --- a/src/network_models/pm_translator.jl +++ b/src/network_models/pm_translator.jl @@ -663,6 +663,27 @@ function get_branch_to_pm( return Dict{String, Any}() end +function get_branch_to_pm( + ix::Int, + branch::PSY.TwoTerminalVSCLine, + ::Type{<:AbstractTwoTerminalVSCFormulation}, + ::Type{<:AbstractPowerModel}, +) + return Dict{String, Any}() +end + +# Trait: should this DeviceModel be skipped when translating two-terminal HVDC +# branches into PowerModels? LCC and VSC formulations are handled by their own +# constraints in POM rather than via PM's built-in DC line model, so they're +# skipped here. Default is `false`; override per formulation via dispatch. +_skip_pm_two_terminal_translation(::DeviceModel) = false +_skip_pm_two_terminal_translation( + ::DeviceModel{PSY.TwoTerminalLCCLine, HVDCTwoTerminalLCC}, +) = true +_skip_pm_two_terminal_translation( + ::DeviceModel{PSY.TwoTerminalVSCLine, <:AbstractTwoTerminalVSCFormulation}, +) = true + function get_branches_to_pm( sys::PSY.System, network_model::NetworkModel{S}, @@ -720,10 +741,7 @@ function get_branches_to_pm( for (d, device_model) in branch_template comp_type = get_component_type(device_model) !(comp_type <: T) && continue - if comp_type <: PSY.TwoTerminalLCCLine && - get_formulation(device_model) <: HVDCTwoTerminalLCC - continue - end + _skip_pm_two_terminal_translation(device_model) && continue start_idx += length(PM_branches) for (i, branch) in enumerate(get_available_components(device_model, sys)) ix = i + start_idx diff --git a/src/operation/decision_model.jl b/src/operation/decision_model.jl index b6ce1b9..be9cadc 100644 --- a/src/operation/decision_model.jl +++ b/src/operation/decision_model.jl @@ -67,7 +67,7 @@ function build!( IOM.register_recorders!(model, file_mode) logger = IS.configure_logging(get_internal(model), IOM.PROBLEM_LOG_FILENAME, file_mode) if store_system_in_results - @warn "store_system_in_results for $(model) is set to true. This will do nothing unless a Simulation is being built." + @warn "store_system_in_results for $(model.name) is set to true. This will do nothing unless a Simulation is being built." end try Logging.with_logger(logger) do @@ -151,7 +151,7 @@ function solve!( kwargs..., ) if store_system_in_results - @warn "store_system_in_results for $(model) is set to true. This will do nothing unless a Simulation is being built." + @warn "store_system_in_results for $(model.name) is set to true. This will do nothing unless a Simulation is being built." end build_if_not_already_built!( model; diff --git a/src/operation/emulation_model.jl b/src/operation/emulation_model.jl index 9241729..6a4d6f3 100644 --- a/src/operation/emulation_model.jl +++ b/src/operation/emulation_model.jl @@ -69,7 +69,7 @@ function build!( file_mode, ) if store_system_in_results - @warn "store_system_in_results for $(model) is set to true. This will do nothing unless a Simulation is being built." + @warn "store_system_in_results for $(model.name) is set to true. This will do nothing unless a Simulation is being built." end try Logging.with_logger(logger) do @@ -197,7 +197,7 @@ function run!( kwargs..., ) if store_system_in_results - @warn "store_system_in_results for $(model) is set to true. This will do nothing unless a Simulation is being built." + @warn "store_system_in_results for $(model.name) is set to true. This will do nothing unless a Simulation is being built." end build_if_not_already_built!( model; diff --git a/src/twoterminal_hvdc_models/TwoTerminalDC_branches.jl b/src/twoterminal_hvdc_models/TwoTerminalDC_branches.jl index 5422b6f..4fa388e 100644 --- a/src/twoterminal_hvdc_models/TwoTerminalDC_branches.jl +++ b/src/twoterminal_hvdc_models/TwoTerminalDC_branches.jl @@ -1433,3 +1433,362 @@ function add_constraints!( end return end + +############################################################################## +####################### Two-Terminal VSC Formulation ######################### +############################################################################## + +#! format: off + +# Variable trait methods for the shared cable current and DC voltages +get_variable_binary(::Type{DCLineCurrentFlowVariable}, ::Type{PSY.TwoTerminalVSCLine}, ::Type{<:AbstractTwoTerminalVSCFormulation}) = false +get_variable_binary(::Type{HVDCFromDCVoltage}, ::Type{PSY.TwoTerminalVSCLine}, ::Type{<:AbstractTwoTerminalVSCFormulation}) = false +get_variable_binary(::Type{HVDCToDCVoltage}, ::Type{PSY.TwoTerminalVSCLine}, ::Type{<:AbstractTwoTerminalVSCFormulation}) = false +get_variable_binary(::Type{HVDCReactivePowerFromVariable}, ::Type{PSY.TwoTerminalVSCLine}, ::Type{<:AbstractTwoTerminalVSCFormulation}) = false +get_variable_binary(::Type{HVDCReactivePowerToVariable}, ::Type{PSY.TwoTerminalVSCLine}, ::Type{<:AbstractTwoTerminalVSCFormulation}) = false +get_variable_binary(::Type{PositiveCurrent}, ::Type{PSY.TwoTerminalVSCLine}, ::Type{<:AbstractTwoTerminalVSCFormulation}) = false +get_variable_binary(::Type{NegativeCurrent}, ::Type{PSY.TwoTerminalVSCLine}, ::Type{<:AbstractTwoTerminalVSCFormulation}) = false +get_variable_binary(::Type{CurrentDirection}, ::Type{PSY.TwoTerminalVSCLine}, ::Type{<:AbstractTwoTerminalVSCFormulation}) = true +get_variable_binary(::Type{FlowActivePowerFromToVariable}, ::Type{PSY.TwoTerminalVSCLine}, ::Type{<:AbstractTwoTerminalVSCFormulation}) = false +get_variable_binary(::Type{FlowActivePowerToFromVariable}, ::Type{PSY.TwoTerminalVSCLine}, ::Type{<:AbstractTwoTerminalVSCFormulation}) = false + +# Warm starts +get_variable_warm_start_value(::Type{DCLineCurrentFlowVariable}, d::PSY.TwoTerminalVSCLine, ::Type{<:AbstractTwoTerminalVSCFormulation}) = PSY.get_dc_current(d) +get_variable_warm_start_value(::Type{HVDCReactivePowerFromVariable}, d::PSY.TwoTerminalVSCLine, ::Type{<:AbstractTwoTerminalVSCFormulation}) = PSY.get_reactive_power_from(d) +get_variable_warm_start_value(::Type{HVDCReactivePowerToVariable}, d::PSY.TwoTerminalVSCLine, ::Type{<:AbstractTwoTerminalVSCFormulation}) = PSY.get_reactive_power_to(d) +get_variable_warm_start_value(::Type{FlowActivePowerFromToVariable}, d::PSY.TwoTerminalVSCLine, ::Type{<:AbstractTwoTerminalVSCFormulation}) = PSY.get_active_power_flow(d) +get_variable_warm_start_value(::Type{FlowActivePowerToFromVariable}, d::PSY.TwoTerminalVSCLine, ::Type{<:AbstractTwoTerminalVSCFormulation}) = -PSY.get_active_power_flow(d) + +# Active power flow bounds (per-terminal) +get_variable_lower_bound(::Type{FlowActivePowerFromToVariable}, d::PSY.TwoTerminalVSCLine, ::Type{<:AbstractTwoTerminalVSCFormulation}) = PSY.get_active_power_limits_from(d).min +get_variable_upper_bound(::Type{FlowActivePowerFromToVariable}, d::PSY.TwoTerminalVSCLine, ::Type{<:AbstractTwoTerminalVSCFormulation}) = PSY.get_active_power_limits_from(d).max +get_variable_lower_bound(::Type{FlowActivePowerToFromVariable}, d::PSY.TwoTerminalVSCLine, ::Type{<:AbstractTwoTerminalVSCFormulation}) = PSY.get_active_power_limits_to(d).min +get_variable_upper_bound(::Type{FlowActivePowerToFromVariable}, d::PSY.TwoTerminalVSCLine, ::Type{<:AbstractTwoTerminalVSCFormulation}) = PSY.get_active_power_limits_to(d).max + +# Reactive power bounds (per-terminal) +get_variable_lower_bound(::Type{HVDCReactivePowerFromVariable}, d::PSY.TwoTerminalVSCLine, ::Type{<:AbstractTwoTerminalVSCFormulation}) = PSY.get_reactive_power_limits_from(d).min +get_variable_upper_bound(::Type{HVDCReactivePowerFromVariable}, d::PSY.TwoTerminalVSCLine, ::Type{<:AbstractTwoTerminalVSCFormulation}) = PSY.get_reactive_power_limits_from(d).max +get_variable_lower_bound(::Type{HVDCReactivePowerToVariable}, d::PSY.TwoTerminalVSCLine, ::Type{<:AbstractTwoTerminalVSCFormulation}) = PSY.get_reactive_power_limits_to(d).min +get_variable_upper_bound(::Type{HVDCReactivePowerToVariable}, d::PSY.TwoTerminalVSCLine, ::Type{<:AbstractTwoTerminalVSCFormulation}) = PSY.get_reactive_power_limits_to(d).max + +# DC voltage bounds (per-terminal) +get_variable_lower_bound(::Type{HVDCFromDCVoltage}, d::PSY.TwoTerminalVSCLine, ::Type{<:AbstractTwoTerminalVSCFormulation}) = PSY.get_voltage_limits_from(d).min +get_variable_upper_bound(::Type{HVDCFromDCVoltage}, d::PSY.TwoTerminalVSCLine, ::Type{<:AbstractTwoTerminalVSCFormulation}) = PSY.get_voltage_limits_from(d).max +get_variable_lower_bound(::Type{HVDCToDCVoltage}, d::PSY.TwoTerminalVSCLine, ::Type{<:AbstractTwoTerminalVSCFormulation}) = PSY.get_voltage_limits_to(d).min +get_variable_upper_bound(::Type{HVDCToDCVoltage}, d::PSY.TwoTerminalVSCLine, ::Type{<:AbstractTwoTerminalVSCFormulation}) = PSY.get_voltage_limits_to(d).max + +# Shared cable current bounds — must respect BOTH terminals' I_max ratings +_vsc_shared_i_max(d::PSY.TwoTerminalVSCLine) = + min(PSY.get_max_dc_current_from(d), PSY.get_max_dc_current_to(d)) +get_variable_lower_bound(::Type{DCLineCurrentFlowVariable}, d::PSY.TwoTerminalVSCLine, ::Type{<:AbstractTwoTerminalVSCFormulation}) = -_vsc_shared_i_max(d) +get_variable_upper_bound(::Type{DCLineCurrentFlowVariable}, d::PSY.TwoTerminalVSCLine, ::Type{<:AbstractTwoTerminalVSCFormulation}) = _vsc_shared_i_max(d) + +# Positive/negative parts: each in [0, i_max] +get_variable_lower_bound(::Type{PositiveCurrent}, d::PSY.TwoTerminalVSCLine, ::Type{<:AbstractTwoTerminalVSCFormulation}) = 0.0 +get_variable_upper_bound(::Type{PositiveCurrent}, d::PSY.TwoTerminalVSCLine, ::Type{<:AbstractTwoTerminalVSCFormulation}) = _vsc_shared_i_max(d) +get_variable_lower_bound(::Type{NegativeCurrent}, d::PSY.TwoTerminalVSCLine, ::Type{<:AbstractTwoTerminalVSCFormulation}) = 0.0 +get_variable_upper_bound(::Type{NegativeCurrent}, d::PSY.TwoTerminalVSCLine, ::Type{<:AbstractTwoTerminalVSCFormulation}) = _vsc_shared_i_max(d) + +#! format: on + +####################### VSC reactive-power optional path ##################### +# The reactive power and PQ capability machinery is added only when the +# network actually models reactive power. On active-only networks these +# helpers are no-ops. + +function _maybe_add_reactive_power_variables!( + container::OptimizationContainer, + devices, + model::DeviceModel{PSY.TwoTerminalVSCLine, F}, + ::NetworkModel{<:AbstractPowerModel}, +) where {F <: AbstractTwoTerminalVSCFormulation} + add_variables!(container, HVDCReactivePowerFromVariable, devices, F) + add_variables!(container, HVDCReactivePowerToVariable, devices, F) + return +end + +_maybe_add_reactive_power_variables!( + ::OptimizationContainer, + _devices, + ::DeviceModel{PSY.TwoTerminalVSCLine, <:AbstractTwoTerminalVSCFormulation}, + ::NetworkModel{<:AbstractActivePowerModel}, +) = nothing + +function _maybe_add_reactive_power_constraints!( + container::OptimizationContainer, + devices, + model::DeviceModel{PSY.TwoTerminalVSCLine, F}, + network_model::NetworkModel{<:AbstractPowerModel}, +) where {F <: AbstractTwoTerminalVSCFormulation} + add_constraints!( + container, HVDCVSCReactiveCapabilityConstraint, + devices, model, network_model, + ) + return +end + +_maybe_add_reactive_power_constraints!( + ::OptimizationContainer, + _devices, + ::DeviceModel{PSY.TwoTerminalVSCLine, <:AbstractTwoTerminalVSCFormulation}, + ::NetworkModel{<:AbstractActivePowerModel}, +) = nothing + +####################### VSC core constraints ################################ + +# Cable Ohm's law: v_f - v_t = (1/g) * I +function add_constraints!( + container::OptimizationContainer, + ::Type{HVDCCableOhmsLawConstraint}, + devices::Union{ + Vector{PSY.TwoTerminalVSCLine}, + IS.FlattenIteratorWrapper{PSY.TwoTerminalVSCLine}, + }, + ::DeviceModel{PSY.TwoTerminalVSCLine, F}, + ::NetworkModel{<:AbstractPowerModel}, +) where {F <: AbstractTwoTerminalVSCFormulation} + time_steps = get_time_steps(container) + names = [PSY.get_name(d) for d in devices] + jump_model = get_jump_model(container) + v_f = get_variable(container, HVDCFromDCVoltage, PSY.TwoTerminalVSCLine) + v_t = get_variable(container, HVDCToDCVoltage, PSY.TwoTerminalVSCLine) + i_var = get_variable(container, DCLineCurrentFlowVariable, PSY.TwoTerminalVSCLine) + + cons = add_constraints_container!( + container, HVDCCableOhmsLawConstraint, PSY.TwoTerminalVSCLine, + names, time_steps, + ) + + for d in devices + name = PSY.get_name(d) + g = PSY.get_g(d) + # If g == 0 we treat the cable as lossless (v_f == v_t). + # Otherwise R = 1/g. + for t in time_steps + cons[name, t] = if iszero(g) + JuMP.@constraint(jump_model, v_f[name, t] == v_t[name, t]) + else + JuMP.@constraint( + jump_model, + v_f[name, t] - v_t[name, t] == (1.0 / g) * i_var[name, t], + ) + end + end + end + return +end + +# Per-terminal converter power balance: +# p_ft == v_f * I + (a_f * I^2 + b_f * |I| + c_f) +# p_tf == -v_t * I + (a_t * I^2 + b_t * |I| + c_t) +# Sign convention: FlowActivePowerFromToVariable / ToFromVariable are positive +# when the corresponding AC bus is sourcing power into the converter (matches +# the existing add_to_expression! method's -1.0 multiplier). +function add_constraints!( + container::OptimizationContainer, + ::Type{HVDCVSCConverterPowerConstraint}, + devices::Union{ + Vector{PSY.TwoTerminalVSCLine}, + IS.FlattenIteratorWrapper{PSY.TwoTerminalVSCLine}, + }, + model::DeviceModel{PSY.TwoTerminalVSCLine, F}, + ::NetworkModel{<:AbstractPowerModel}, +) where {F <: AbstractTwoTerminalVSCFormulation} + time_steps = get_time_steps(container) + names = [PSY.get_name(d) for d in devices] + jump_model = get_jump_model(container) + + p_ft = get_variable(container, FlowActivePowerFromToVariable, PSY.TwoTerminalVSCLine) + p_tf = get_variable(container, FlowActivePowerToFromVariable, PSY.TwoTerminalVSCLine) + vi_expr = get_expression( + container, + IOM.BilinearProductExpression, + PSY.TwoTerminalVSCLine, + "vi_ft", + ) + vi_expr_to = get_expression( + container, + IOM.BilinearProductExpression, + PSY.TwoTerminalVSCLine, + "vi_tf", + ) + i_sq_expr = + get_expression(container, IOM.QuadraticExpression, PSY.TwoTerminalVSCLine, "i_sq") + + use_linear_loss = + _use_linear_loss(F, model) && !isempty(_devices_with_linear_loss(devices)) + if use_linear_loss + i_pos_var = get_variable(container, PositiveCurrent, PSY.TwoTerminalVSCLine) + i_neg_var = get_variable(container, NegativeCurrent, PSY.TwoTerminalVSCLine) + end + + cons_ft = add_constraints_container!( + container, HVDCVSCConverterPowerConstraint, PSY.TwoTerminalVSCLine, + names, time_steps; meta = "ft", + ) + cons_tf = add_constraints_container!( + container, HVDCVSCConverterPowerConstraint, PSY.TwoTerminalVSCLine, + names, time_steps; meta = "tf", + ) + + for d in devices + name = PSY.get_name(d) + loss_from = PSY.get_converter_loss_from(d) + loss_to = PSY.get_converter_loss_to(d) + a_f = _get_quadratic_term(loss_from) + b_f = PSY.get_proportional_term(loss_from) + c_f = PSY.get_constant_term(loss_from) + a_t = _get_quadratic_term(loss_to) + b_t = PSY.get_proportional_term(loss_to) + c_t = PSY.get_constant_term(loss_to) + for t in time_steps + i_pos_t = use_linear_loss ? i_pos_var[name, t] : nothing + i_neg_t = use_linear_loss ? i_neg_var[name, t] : nothing + loss_ft = _quadratic_converter_loss_expr( + a_f, b_f, c_f, i_sq_expr[name, t], i_pos_t, i_neg_t; + use_linear_loss = use_linear_loss, + ) + loss_tf = _quadratic_converter_loss_expr( + a_t, b_t, c_t, i_sq_expr[name, t], i_pos_t, i_neg_t; + use_linear_loss = use_linear_loss, + ) + cons_ft[name, t] = JuMP.@constraint( + jump_model, + p_ft[name, t] == vi_expr[name, t] + loss_ft, + ) + cons_tf[name, t] = JuMP.@constraint( + jump_model, + p_tf[name, t] == -vi_expr_to[name, t] + loss_tf, + ) + end + end + return +end + +# PQ capability: p_k^2 + q_k^2 <= S_k^2 (NLP) or octagonal polygon (Bin2). +function add_constraints!( + container::OptimizationContainer, + ::Type{HVDCVSCReactiveCapabilityConstraint}, + devices::Union{ + Vector{PSY.TwoTerminalVSCLine}, + IS.FlattenIteratorWrapper{PSY.TwoTerminalVSCLine}, + }, + ::DeviceModel{PSY.TwoTerminalVSCLine, HVDCTwoTerminalVSC}, + ::NetworkModel{<:AbstractPowerModel}, +) + time_steps = get_time_steps(container) + names = [PSY.get_name(d) for d in devices] + jump_model = get_jump_model(container) + + p_ft = get_variable(container, FlowActivePowerFromToVariable, PSY.TwoTerminalVSCLine) + p_tf = get_variable(container, FlowActivePowerToFromVariable, PSY.TwoTerminalVSCLine) + q_f = get_variable(container, HVDCReactivePowerFromVariable, PSY.TwoTerminalVSCLine) + q_t = get_variable(container, HVDCReactivePowerToVariable, PSY.TwoTerminalVSCLine) + + cons_f = add_constraints_container!( + container, HVDCVSCReactiveCapabilityConstraint, PSY.TwoTerminalVSCLine, + names, time_steps; meta = "from", + ) + cons_t = add_constraints_container!( + container, HVDCVSCReactiveCapabilityConstraint, PSY.TwoTerminalVSCLine, + names, time_steps; meta = "to", + ) + + for d in devices + name = PSY.get_name(d) + s_f = PSY.get_rating_from(d) + s_t = PSY.get_rating_to(d) + for t in time_steps + cons_f[name, t] = JuMP.@constraint( + jump_model, + p_ft[name, t]^2 + q_f[name, t]^2 <= s_f^2, + ) + cons_t[name, t] = JuMP.@constraint( + jump_model, + p_tf[name, t]^2 + q_t[name, t]^2 <= s_t^2, + ) + end + end + return +end + +# Bin2 variant: inscribed octagon in the rating circle (s/sqrt(2) half-diagonal). +# Eight linear constraints per terminal: ±p ± q ≤ s and ±p ≤ s, ±q ≤ s. +function add_constraints!( + container::OptimizationContainer, + ::Type{HVDCVSCReactiveCapabilityConstraint}, + devices::Union{ + Vector{PSY.TwoTerminalVSCLine}, + IS.FlattenIteratorWrapper{PSY.TwoTerminalVSCLine}, + }, + ::DeviceModel{PSY.TwoTerminalVSCLine, HVDCTwoTerminalVSCBin2}, + ::NetworkModel{<:AbstractPowerModel}, +) + time_steps = get_time_steps(container) + names = [PSY.get_name(d) for d in devices] + jump_model = get_jump_model(container) + + p_ft = get_variable(container, FlowActivePowerFromToVariable, PSY.TwoTerminalVSCLine) + p_tf = get_variable(container, FlowActivePowerToFromVariable, PSY.TwoTerminalVSCLine) + q_f = get_variable(container, HVDCReactivePowerFromVariable, PSY.TwoTerminalVSCLine) + q_t = get_variable(container, HVDCReactivePowerToVariable, PSY.TwoTerminalVSCLine) + + cons = Dict{String, Any}() + for tag in ("from_pp", "from_pn", "from_np", "from_nn", + "to_pp", "to_pn", "to_np", "to_nn") + cons[tag] = add_constraints_container!( + container, HVDCVSCReactiveCapabilityConstraint, PSY.TwoTerminalVSCLine, + names, time_steps; meta = tag, + ) + end + + inv_sqrt2 = 1.0 / sqrt(2.0) + for d in devices + name = PSY.get_name(d) + s_f = PSY.get_rating_from(d) * inv_sqrt2 + s_t = PSY.get_rating_to(d) * inv_sqrt2 + for t in time_steps + # Octagon at the from terminal: project (p, q) onto 4 diagonal lines. + cons["from_pp"][name, t] = + JuMP.@constraint(jump_model, p_ft[name, t] + q_f[name, t] <= 2.0 * s_f) + cons["from_pn"][name, t] = + JuMP.@constraint(jump_model, p_ft[name, t] - q_f[name, t] <= 2.0 * s_f) + cons["from_np"][name, t] = + JuMP.@constraint(jump_model, -p_ft[name, t] + q_f[name, t] <= 2.0 * s_f) + cons["from_nn"][name, t] = + JuMP.@constraint(jump_model, -p_ft[name, t] - q_f[name, t] <= 2.0 * s_f) + cons["to_pp"][name, t] = + JuMP.@constraint(jump_model, p_tf[name, t] + q_t[name, t] <= 2.0 * s_t) + cons["to_pn"][name, t] = + JuMP.@constraint(jump_model, p_tf[name, t] - q_t[name, t] <= 2.0 * s_t) + cons["to_np"][name, t] = + JuMP.@constraint(jump_model, -p_tf[name, t] + q_t[name, t] <= 2.0 * s_t) + cons["to_nn"][name, t] = + JuMP.@constraint(jump_model, -p_tf[name, t] - q_t[name, t] <= 2.0 * s_t) + end + end + return +end + +####################### VSC defaults ######################################### + +function get_default_time_series_names( + ::Type{PSY.TwoTerminalVSCLine}, + ::Type{<:AbstractTwoTerminalVSCFormulation}, +) + return Dict{Type{<:TimeSeriesParameter}, String}() +end + +function get_default_attributes( + ::Type{PSY.TwoTerminalVSCLine}, + ::Type{HVDCTwoTerminalVSC}, +) + return Dict{String, Any}("use_linear_loss" => false) +end + +function get_default_attributes( + ::Type{PSY.TwoTerminalVSCLine}, + ::Type{HVDCTwoTerminalVSCBin2}, +) + return Dict{String, Any}() +end diff --git a/test/Project.toml b/test/Project.toml index 9445668..43494b2 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -32,10 +32,10 @@ TimerOutputs = "a759f4b9-e2f1-59dc-863e-4aeb61b1ea8f" UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" [sources] -InfrastructureSystems = {url = "https://github.com/NREL-Sienna/InfrastructureSystems.jl", rev = "IS4"} -PowerSystems = {url = "https://github.com/NREL-Sienna/PowerSystems.jl", rev = "psy6"} -InfrastructureOptimizationModels = {url = "https://github.com/NREL-Sienna/InfrastructureOptimizationModels.jl", rev = "lk/pom-test-fixes"} -PowerSystemCaseBuilder = {url = "https://github.com/NREL-Sienna/PowerSystemCaseBuilder.jl", rev = "psy6"} +InfrastructureSystems = {rev = "IS4", url = "https://github.com/Sienna-Platform/InfrastructureSystems.jl"} +PowerSystemCaseBuilder = {rev = "psy6", url = "https://github.com/Sienna-Platform/PowerSystemCaseBuilder.jl"} +PowerSystems = {rev = "psy6", url = "https://github.com/Sienna-Platform/PowerSystems.jl"} +InfrastructureOptimizationModels = {rev = "ac/hvdc-vsc", url = "https://github.com/Sienna-Platform/InfrastructureOptimizationModels.jl"} [compat] HiGHS = "1" diff --git a/test/runtests.jl b/test/runtests.jl index 7d8fc42..d290015 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -22,8 +22,14 @@ const DISABLED_TEST_FILES = [ # Can generate with ls -1 test | grep "test_.*.jl # "test_device_synchronous_condenser_constructors.jl", # "test_device_thermal_generation_constructors.jl", # "test_formulation_combinations.jl", +# "test_import_export_cost.jl", # "test_initialization_problem.jl", +# "test_is_time_variant_proportional.jl", +# "test_market_bid_cost.jl", +# "test_mbc_parameter_population.jl", # "test_model_decision.jl", +# "test_multi_interval.jl", +# "test_network_constructors.jl", # "test_network_constructors_with_dlr.jl", # "test_problem_template.jl", # "test_storage_device_models.jl", diff --git a/test/test_device_hvdc.jl b/test/test_device_hvdc.jl index 2b158b3..08f6f63 100644 --- a/test/test_device_hvdc.jl +++ b/test/test_device_hvdc.jl @@ -97,7 +97,7 @@ end @test solve!(model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED end -@testset "HVDC System with Losses Network" begin +@testset "HVDC System with Losses Network (Bin2QuadraticLossConverter)" begin sys = _generate_test_hvdc_sys() template = OperationsProblemTemplate() set_device_model!(template, ThermalStandard, ThermalDispatchNoMin) @@ -106,13 +106,7 @@ end set_device_model!(template, TModelHVDCLine, DCLossyLine) ipc_model = DeviceModel( InterconnectingConverter, - QuadraticLossConverter; - attributes = Dict( - "voltage_segments" => 3, - "current_segments" => 3, - "bilinear_segments" => 3, - "use_linear_loss" => true, - ), + Bin2QuadraticLossConverter, ) set_device_model!(template, ipc_model) set_hvdc_network_model!(template, VoltageDispatchHVDCNetworkModel) @@ -127,3 +121,234 @@ end IOM.ModelBuildStatus.BUILT @test solve!(model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED end + +@testset "HVDC System with Losses Network (QuadraticLossConverter NLP)" begin + sys = _generate_test_hvdc_sys() + template = OperationsProblemTemplate() + set_device_model!(template, ThermalStandard, ThermalDispatchNoMin) + set_device_model!(template, PowerLoad, StaticPowerLoad) + set_device_model!(template, DeviceModel(Line, StaticBranch)) + set_device_model!(template, TModelHVDCLine, DCLossyLine) + ipc_model = DeviceModel( + InterconnectingConverter, + QuadraticLossConverter, + ) + set_device_model!(template, ipc_model) + set_hvdc_network_model!(template, VoltageDispatchHVDCNetworkModel) + model = + DecisionModel( + template, + sys; + store_variable_names = true, + optimizer = ipopt_optimizer, + ) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT + @test solve!(model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED +end + +@testset "HVDC Bin2 vs NLP QuadraticLossConverter agreement" begin + function _build_and_solve(formulation, optimizer) + sys = _generate_test_hvdc_sys() + template = OperationsProblemTemplate() + set_device_model!(template, ThermalStandard, ThermalDispatchNoMin) + set_device_model!(template, PowerLoad, StaticPowerLoad) + set_device_model!(template, DeviceModel(Line, StaticBranch)) + set_device_model!(template, TModelHVDCLine, DCLossyLine) + set_device_model!( + template, + DeviceModel( + InterconnectingConverter, + formulation; + attributes = Dict("use_linear_loss" => false), + ), + ) + set_hvdc_network_model!(template, VoltageDispatchHVDCNetworkModel) + model = DecisionModel( + template, + sys; + store_variable_names = true, + optimizer = optimizer, + ) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT + @test solve!(model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED + return model + end + + bin2_model = _build_and_solve(Bin2QuadraticLossConverter, HiGHS_optimizer) + nlp_model = _build_and_solve(QuadraticLossConverter, ipopt_optimizer) + + bin2_obj = IOM.get_objective_value(OptimizationProblemOutputs(bin2_model)) + nlp_obj = IOM.get_objective_value(OptimizationProblemOutputs(nlp_model)) + + # Bin2 is a relaxation/PWL approximation of the exact NLP. The two objectives + # should agree to within a few percent on this small system. + @test isapprox(bin2_obj, nlp_obj; rtol = 0.05) +end + +@testset "HVDC linear-loss warning when all converters have b=0" begin + sys = _generate_test_hvdc_sys() + for ipc in get_components(InterconnectingConverter, sys) + set_loss_function!(ipc, QuadraticCurve(0.01, 0.0, 0.0)) + end + template = OperationsProblemTemplate() + set_device_model!(template, ThermalStandard, ThermalDispatchNoMin) + set_device_model!(template, PowerLoad, StaticPowerLoad) + set_device_model!(template, DeviceModel(Line, StaticBranch)) + set_device_model!(template, TModelHVDCLine, DCLossyLine) + set_device_model!( + template, + DeviceModel(InterconnectingConverter, Bin2QuadraticLossConverter), + ) + set_hvdc_network_model!(template, VoltageDispatchHVDCNetworkModel) + model = DecisionModel( + template, + sys; + store_variable_names = true, + optimizer = HiGHS_optimizer, + ) + # `build!` wraps its body in `Logging.with_logger(file_logger)`, which masks + # any `TestLogger` set up by `@test_logs`. The warning we want lands in the + # per-build log file instead, so read it back from there. + output_dir = mktempdir(; cleanup = true) + @test build!(model; output_dir = output_dir) == IOM.ModelBuildStatus.BUILT + log_path = joinpath(output_dir, IOM.PROBLEM_LOG_FILENAME) + @test occursin(r"linear[_ ]loss"i, read(log_path, String)) +end + +############################################################################## +################ Two-Terminal VSC HVDC tests ################################# +############################################################################## + +# Build a small AC test system and replace an AC line with a TwoTerminalVSCLine +# so we have a concrete VSC device to exercise the formulation against. +function _generate_test_vsc_sys(; + g = 50.0, + rating_from = 2.0, + rating_to = 2.0, + loss_a = 0.01, + loss_b = 0.0, + loss_c = 0.0, +) + sys = build_system(PSITestSystems, "c_sys5_uc"; force_build = true) + line = get_component(Line, sys, "1") + remove_component!(sys, line) + + vsc = TwoTerminalVSCLine(; + name = get_name(line), + available = true, + arc = get_arc(line), + active_power_flow = 0.0, + rating = max(rating_from, rating_to), + active_power_limits_from = (min = -rating_from, max = rating_from), + active_power_limits_to = (min = -rating_to, max = rating_to), + g = g, + dc_current = 0.0, + reactive_power_from = 0.0, + dc_voltage_control_from = true, + ac_voltage_control_from = true, + dc_setpoint_from = 0.0, + ac_setpoint_from = 1.0, + converter_loss_from = QuadraticCurve(loss_a, loss_b, loss_c), + max_dc_current_from = 5.0, + rating_from = rating_from, + reactive_power_limits_from = (min = -rating_from, max = rating_from), + power_factor_weighting_fraction_from = 1.0, + voltage_limits_from = (min = 0.95, max = 1.05), + reactive_power_to = 0.0, + dc_voltage_control_to = true, + ac_voltage_control_to = true, + dc_setpoint_to = 0.0, + ac_setpoint_to = 1.0, + converter_loss_to = QuadraticCurve(loss_a, loss_b, loss_c), + max_dc_current_to = 5.0, + rating_to = rating_to, + reactive_power_limits_to = (min = -rating_to, max = rating_to), + power_factor_weighting_fraction_to = 1.0, + voltage_limits_to = (min = 0.95, max = 1.05), + ) + add_component!(sys, vsc) + return sys +end + +function _build_vsc_model(formulation, network, optimizer; sys = _generate_test_vsc_sys()) + template = OperationsProblemTemplate(NetworkModel(network)) + set_device_model!(template, ThermalStandard, ThermalDispatchNoMin) + set_device_model!(template, RenewableDispatch, RenewableFullDispatch) + set_device_model!(template, PowerLoad, StaticPowerLoad) + set_device_model!(template, DeviceModel(Line, StaticBranch)) + set_device_model!(template, DeviceModel(TwoTerminalVSCLine, formulation)) + return DecisionModel( + template, sys; store_variable_names = true, optimizer = optimizer, + ) +end + +@testset "HVDC Two-Terminal VSC (Bin2) on DCP" begin + model = _build_vsc_model(HVDCTwoTerminalVSCBin2, DCPPowerModel, HiGHS_optimizer) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT + @test solve!(model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED +end + +@testset "HVDC Two-Terminal VSC (NLP) on DCP" begin + model = _build_vsc_model(HVDCTwoTerminalVSC, DCPPowerModel, ipopt_optimizer) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT + @test solve!(model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED +end + +@testset "HVDC Two-Terminal VSC on AC (NLP)" begin + model = _build_vsc_model(HVDCTwoTerminalVSC, ACPPowerModel, ipopt_optimizer) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT + @test solve!(model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED +end + +@testset "HVDC VSC Bin2 vs NLP objective agreement" begin + function _solve(formulation, optimizer) + sys = _generate_test_vsc_sys() + model = _build_vsc_model(formulation, DCPPowerModel, optimizer; sys = sys) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT + @test solve!(model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED + return IOM.get_optimization_container(model).optimizer_stats.objective_value + end + bin2_obj = _solve(HVDCTwoTerminalVSCBin2, HiGHS_optimizer) + nlp_obj = _solve(HVDCTwoTerminalVSC, ipopt_optimizer) + # Bin2 PWL-approximates the same physics — objectives should agree closely. + @test isapprox(bin2_obj, nlp_obj; rtol = 0.05) +end + +@testset "HVDC VSC: higher cable resistance increases cost" begin + # Smaller g => larger R = 1/g => more losses => optimum should not improve. + function _solve_with_g(g_value) + sys = _generate_test_vsc_sys(; g = g_value) + model = _build_vsc_model( + HVDCTwoTerminalVSCBin2, DCPPowerModel, HiGHS_optimizer; sys = sys, + ) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT + @test solve!(model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED + return IOM.get_optimization_container(model).optimizer_stats.objective_value + end + low_R_obj = _solve_with_g(100.0) # large g, small R + high_R_obj = _solve_with_g(20.0) # smaller g, larger R + @test high_R_obj >= low_R_obj - 1e-6 +end + +@testset "HVDC VSC: tighter PQ rating raises cost on AC" begin + function _solve_with_rating(s) + sys = _generate_test_vsc_sys(; rating_from = s, rating_to = s) + model = _build_vsc_model( + HVDCTwoTerminalVSC, ACPPowerModel, ipopt_optimizer; sys = sys, + ) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT + @test solve!(model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED + return IOM.get_optimization_container(model).optimizer_stats.objective_value + end + looser = _solve_with_rating(2.0) + tighter = _solve_with_rating(1.0) + @test tighter >= looser - 1e-6 +end