diff --git a/src/InfrastructureOptimizationModels.jl b/src/InfrastructureOptimizationModels.jl index a5e5f6a..d6a37d6 100644 --- a/src/InfrastructureOptimizationModels.jl +++ b/src/InfrastructureOptimizationModels.jl @@ -481,7 +481,7 @@ export get_system_to_file, get_initialize_model, get_initialization_file export get_deserialize_initial_conditions, get_export_pwl_vars export get_check_numerical_bounds, get_allow_fails export get_optimizer_solve_log_print, get_calculate_conflict -export get_detailed_optimizer_stats, get_direct_mode_optimizer +export get_detailed_optimizer_stats, get_direct_model_optimizer export get_store_variable_names, get_export_optimization_model export use_time_series_cache export set_horizon!, set_initial_time!, set_warm_start! diff --git a/src/core/optimization_container.jl b/src/core/optimization_container.jl index ec5ba1f..0d6e552 100644 --- a/src/core/optimization_container.jl +++ b/src/core/optimization_container.jl @@ -95,7 +95,7 @@ function OptimizationContainer( error("Default Time Series Type $T can't be abstract") end - if jump_model !== nothing && get_direct_mode_optimizer(settings) + if jump_model !== nothing && get_direct_model_optimizer(settings) throw( IS.ConflictingInputsError( "Externally provided JuMP models are not compatible with the direct model keyword argument. Use JuMP.direct_model before passing the custom model", @@ -302,7 +302,7 @@ function _finalize_jump_model!(container::OptimizationContainer, settings::Setti ) end - if get_direct_mode_optimizer(settings) + if get_direct_model_optimizer(settings) optimizer = () -> MOI.instantiate(get_optimizer(settings)) container.JuMPmodel = JuMP.direct_model(optimizer()) elseif get_optimizer(settings) === nothing @@ -332,18 +332,28 @@ function _finalize_jump_model!(container::OptimizationContainer, settings::Setti return end +function intermediate_set_units_base_system!(sys::PSY.System, base) + PSY.set_units_base_system(sys, "SYSTEM_BASE") +end + +function intermediate_get_forecast_initial_timestamp(sys::PSY.System) + return PSY.get_forecast_initial_timestamp(sys) +end + function init_optimization_container!( container::OptimizationContainer, network_model::NetworkModel{T}, - sys::PSY.System, + sys::IS.InfrastructureSystemsContainer, ) where {T <: AbstractPowerModel} - PSY.set_units_base_system!(sys, "SYSTEM_BASE") + # PSY.set_units_base_system!(sys, "SYSTEM_BASE") + intermediate_set_units_base_system!(sys, "SYSTEM_BASE") # The order of operations matter settings = get_settings(container) if get_initial_time(settings) == UNSET_INI_TIME if get_default_time_series_type(container) <: PSY.AbstractDeterministic - set_initial_time!(settings, PSY.get_forecast_initial_timestamp(sys)) + # set_initial_time!(settings, PSY.get_forecast_initial_timestamp(sys)) + set_initial_time!(settings, intermediate_get_forecast_initial_timestamp(sys)) elseif get_default_time_series_type(container) <: PSY.SingleTimeSeries ini_time, _ = PSY.check_time_series_consistency(sys, PSY.SingleTimeSeries) set_initial_time!(settings, ini_time) @@ -437,7 +447,10 @@ end Execute the optimizer on the container's JuMP model, compute aux/dual variables, and return the run status. Called `solve_impl!(container, system)` in PSI. """ -function execute_optimizer!(container::OptimizationContainer, system::PSY.System) +function execute_optimizer!( + container::OptimizationContainer, + system::IS.InfrastructureSystemsContainer, +) optimizer_stats = get_optimizer_stats(container) jump_model = get_jump_model(container) diff --git a/src/core/settings.jl b/src/core/settings.jl index d8a1386..3c834eb 100644 --- a/src/core/settings.jl +++ b/src/core/settings.jl @@ -5,7 +5,7 @@ struct Settings warm_start::Base.RefValue{Bool} initial_time::Base.RefValue{Dates.DateTime} optimizer::Any # Union{Nothing, MOI.OptimizerWithAttributes} or duck-typed optimizer - direct_mode_optimizer::Bool + direct_model_optimizer::Bool optimizer_solve_log_print::Bool detailed_optimizer_stats::Bool calculate_conflict::Bool @@ -30,7 +30,7 @@ function Settings( horizon::Dates.Period = UNSET_HORIZON, resolution::Dates.Period = UNSET_RESOLUTION, optimizer = nothing, - direct_mode_optimizer::Bool = false, + direct_model_optimizer::Bool = false, optimizer_solve_log_print::Bool = false, detailed_optimizer_stats::Bool = false, calculate_conflict::Bool = false, @@ -65,7 +65,7 @@ function Settings( Ref(warm_start), Ref(initial_time), optimizer_, - direct_mode_optimizer, + direct_model_optimizer, optimizer_solve_log_print, detailed_optimizer_stats, calculate_conflict, @@ -148,7 +148,7 @@ get_allow_fails(settings::Settings) = settings.allow_fails get_optimizer_solve_log_print(settings::Settings) = settings.optimizer_solve_log_print get_calculate_conflict(settings::Settings) = settings.calculate_conflict get_detailed_optimizer_stats(settings::Settings) = settings.detailed_optimizer_stats -get_direct_mode_optimizer(settings::Settings) = settings.direct_mode_optimizer +get_direct_model_optimizer(settings::Settings) = settings.direct_model_optimizer get_store_variable_names(settings::Settings) = settings.store_variable_names get_rebuild_model(settings::Settings) = settings.rebuild_model get_export_optimization_model(settings::Settings) = settings.export_optimization_model diff --git a/src/operation/decision_model.jl b/src/operation/decision_model.jl index 82db6ff..167a3fa 100644 --- a/src/operation/decision_model.jl +++ b/src/operation/decision_model.jl @@ -49,7 +49,7 @@ Build the optimization problem of type M with the specific system and template. - `optimizer_solve_log_print::Bool = false`: Uses JuMP.unset_silent() to print the optimizer's log. By default all solvers are set to MOI.Silent() - `detailed_optimizer_stats::Bool = false`: True to save detailed optimizer stats log. - `calculate_conflict::Bool = false`: True to use solver to calculate conflicts for infeasible problems. Only specific solvers are able to calculate conflicts. - - `direct_mode_optimizer::Bool = false`: True to use the solver in direct mode. Creates a [JuMP.direct_model](https://jump.dev/JuMP.jl/dev/reference/models/#JuMP.direct_model). + - `direct_model_optimizer::Bool = false`: True to use the solver in direct mode. Creates a [JuMP.direct_model](https://jump.dev/JuMP.jl/dev/reference/models/#JuMP.direct_model). - `store_variable_names::Bool = false`: to store variable names in optimization model. Decreases the build times. - `rebuild_model::Bool = false`: It will force the rebuild of the underlying JuMP model with each call to update the model. It increases solution times, use only if the model can't be updated in memory. - `initial_time::Dates.DateTime = UNSET_INI_TIME`: Initial Time for the model solve. @@ -112,7 +112,7 @@ function DecisionModel{M}( optimizer_solve_log_print = false, detailed_optimizer_stats = false, calculate_conflict = false, - direct_mode_optimizer = false, + direct_model_optimizer = false, store_variable_names = false, rebuild_model = false, export_optimization_model = false, @@ -137,7 +137,7 @@ function DecisionModel{M}( calculate_conflict = calculate_conflict, optimizer_solve_log_print = optimizer_solve_log_print, detailed_optimizer_stats = detailed_optimizer_stats, - direct_mode_optimizer = direct_mode_optimizer, + direct_model_optimizer = direct_model_optimizer, check_numerical_bounds = check_numerical_bounds, store_variable_names = store_variable_names, rebuild_model = rebuild_model, diff --git a/src/operation/emulation_model.jl b/src/operation/emulation_model.jl index 1ac6812..a7c005c 100644 --- a/src/operation/emulation_model.jl +++ b/src/operation/emulation_model.jl @@ -39,7 +39,7 @@ Build the optimization problem of type M with the specific system and template. - `calculate_conflict::Bool = false`: True to use solver to calculate conflicts for infeasible problems. Only specific solvers are able to calculate conflicts. - `optimizer_solve_log_print::Bool = false`: Uses JuMP.unset_silent() to print the optimizer's log. By default all solvers are set to MOI.Silent() - `detailed_optimizer_stats::Bool = false`: True to save detailed optimizer stats log. - - `direct_mode_optimizer::Bool = false`: True to use the solver in direct mode. Creates a [JuMP.direct_model](https://jump.dev/JuMP.jl/dev/reference/models/#JuMP.direct_model). + - `direct_model_optimizer::Bool = false`: True to use the solver in direct mode. Creates a [JuMP.direct_model](https://jump.dev/JuMP.jl/dev/reference/models/#JuMP.direct_model). - `store_variable_names::Bool = false`: True to store variable names in optimization model. - `rebuild_model::Bool = false`: It will force the rebuild of the underlying JuMP model with each call to update the model. It increases solution times, use only if the model can't be updated in memory. - `initial_time::Dates.DateTime = UNSET_INI_TIME`: Initial Time for the model solve. @@ -106,7 +106,7 @@ function EmulationModel{M}( calculate_conflict = false, optimizer_solve_log_print = false, detailed_optimizer_stats = false, - direct_mode_optimizer = false, + direct_model_optimizer = false, check_numerical_bounds = true, store_variable_names = false, rebuild_model = false, @@ -128,7 +128,7 @@ function EmulationModel{M}( calculate_conflict = calculate_conflict, optimizer_solve_log_print = optimizer_solve_log_print, detailed_optimizer_stats = detailed_optimizer_stats, - direct_mode_optimizer = direct_mode_optimizer, + direct_model_optimizer = direct_model_optimizer, check_numerical_bounds = check_numerical_bounds, store_variable_names = store_variable_names, rebuild_model = rebuild_model, diff --git a/src/operation/operation_model_interface.jl b/src/operation/operation_model_interface.jl index da670c9..1178917 100644 --- a/src/operation/operation_model_interface.jl +++ b/src/operation/operation_model_interface.jl @@ -263,7 +263,7 @@ function _pre_solve_model_checks(model::OperationModel, optimizer = nothing) error("No Optimizer has been defined, can't solve the operational problem") end else - @assert get_direct_mode_optimizer(get_settings(model)) + @assert get_direct_model_optimizer(get_settings(model)) end optimizer_name = JuMP.solver_name(jump_model) diff --git a/src/quadratic_approximations/nonlinear.jl b/src/quadratic_approximations/nonlinear.jl new file mode 100644 index 0000000..671fb24 --- /dev/null +++ b/src/quadratic_approximations/nonlinear.jl @@ -0,0 +1,27 @@ +struct BilinearProductExpression <: ExpressionType end + +function _add_bilinear!( + container::OptimizationContainer, + ::Type{C}, + names::Vector{String}, + time_steps::UnitRange{Int}, + x_var_container, + y_var_container, + meta::String; +) where {C <: IS.InfrastructureSystemsComponent} + z_container = add_expression_container!( + container, + BilinearProductExpression(), + C, + names, + time_steps; + expr_type = JuMP.QuadExpr, + meta, + ) + for name in names, t in time_steps + z_expr = JuMP.QuadExpr() + JuMP.add_to_expression!(z_expr, x_var_container[name, t], y_var_container[name, t]) + z_container[name, t] = z_expr + end + return z_container +end diff --git a/test/Project.toml b/test/Project.toml index dd1e7fb..6ff8e8c 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -6,7 +6,9 @@ DataFramesMeta = "1313f7d8-7da2-5740-9ea0-a2ca25f37964" DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b" +InfrastructureOptimizationModels = "bed98974-b02a-5e2f-9ee0-a103f5c45069" InfrastructureSystems = "2cd47ed4-ca9b-11e9-27f2-ab636a7671f1" +Ipopt = "b6b21f68-93f8-5de0-b562-5493be1d77c9" JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" @@ -24,9 +26,9 @@ TimeSeries = "9e3dc215-6440-5c97-bce1-76c03772f85e" TimerOutputs = "a759f4b9-e2f1-59dc-863e-4aeb61b1ea8f" UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" +[sources] +InfrastructureSystems = {rev = "IS4", url = "https://github.com/NREL-Sienna/InfrastructureSystems.jl"} + [compat] HiGHS = "1" julia = "^1.10" - -[sources] -InfrastructureSystems = {url = "https://github.com/NREL-Sienna/InfrastructureSystems.jl", rev = "IS4"} diff --git a/test/mocks/constructors.jl b/test/mocks/constructors.jl index b5bb273..7ce7b01 100644 --- a/test/mocks/constructors.jl +++ b/test/mocks/constructors.jl @@ -3,6 +3,7 @@ Factory functions for quickly creating test fixtures. """ using Dates +using Random """ Create a mock system with specified number of buses, generators, and loads. @@ -72,7 +73,87 @@ function make_mock_thermal( bus = MockBus("bus1", 1, :PV), limits = (min = 0.0, max = 100.0), base_power = 100.0, - operation_cost = MockOperationCost(0.0), + operation_cost = MockProportionalCost(0.0), ) return MockThermalGen(name, available, bus, limits, base_power, operation_cost) end + +""" +Generate convex piecewise-linear cost curve points with `n_tranches` segments over [0, pmax]. +Returns a vector of (x, y) tuples with strictly increasing slopes. +""" +function _random_convex_pwl_points(n_tranches::Int, pmax::Float64, rng) + xs = sort(rand(rng, n_tranches - 1)) .* pmax + points = [(0.0, 0.0)] + cumulative_cost = 0.0 + prev_x = 0.0 + slope = 5.0 + 20.0 * rand(rng) + for x in xs + cumulative_cost += slope * (x - prev_x) + push!(points, (x, cumulative_cost)) + prev_x = x + slope += 5.0 + 10.0 * rand(rng) + end + cumulative_cost += slope * (pmax - prev_x) + push!(points, (pmax, cumulative_cost)) + return points +end + +""" +Create a mock system on `n` nodes where the graph is connected (and fairly random), +half the nodes have generators, and half have loads. +""" +function make_mock_test_network(n::Int; max_tranches::Int = 4, seed::Int = 42) + @assert n >= 2 "Need at least 2 nodes for a network" + rng = MersenneTwister(seed) + sys = MockSystem(1.0) + + # Create buses + buses = [MockBus("bus$i", i, :PV) for i in 1:n] + for bus in buses + add_component!(sys, bus) + end + + # Build a connected graph: chain 1-2-3-...-n plus random extra edges + branch_pairs = Set{Tuple{Int, Int}}() + perm = shuffle(rng, 1:n) + for i in 1:(n - 1) + push!(branch_pairs, (perm[i], perm[i + 1])) + end + # Add some random cross-links for variety + n_extra = div(n, 3) + for _ in 1:n_extra + a, b = rand(rng, 1:n), rand(rng, 1:n) + if a != b + pair = a < b ? (a, b) : (b, a) + push!(branch_pairs, pair) + end + end + for (idx, (i, j)) in enumerate(branch_pairs) + r = 0.005 + 0.005 * rand(rng) + branch = MockBranch("branch$idx", true, buses[i], buses[j], 1.0, r) + add_component!(sys, branch) + end + + # Half the nodes get generators with convex PWL costs, the other half get loads + n_gens = div(n, 2) + for i in 1:n_gens + n_tranches = rand(rng, 2:max_tranches) + pmax = 1.5 * rand(rng) + points = _random_convex_pwl_points(n_tranches, pmax, rng) + + pwl = IS.PiecewiseLinearData(points) + cost_curve = IS.CostCurve(IS.InputOutputCurve(pwl)) + op_cost = MockOperationalCost(cost_curve, 0.0, 0.0) + gen = MockThermalGen( + "gen$i", true, buses[i], (min = 0.0, max = pmax), 1.0, op_cost, + ) + add_component!(sys, gen) + end + for i in (n_gens + 1):n + load = MockLoad("load$(i - n_gens)", true, buses[i], 0.5) + add_component!(sys, load) + end + + return sys +end diff --git a/test/mocks/mock_components.jl b/test/mocks/mock_components.jl index 5234d85..0e36982 100644 --- a/test/mocks/mock_components.jl +++ b/test/mocks/mock_components.jl @@ -16,17 +16,31 @@ const IS = InfrastructureSystems # Mock formulation type for testing DeviceModel struct TestDeviceFormulation <: PSI.AbstractDeviceFormulation end +abstract type AbstractMockCost end + # Mock operation cost for testing proportional cost functions -struct MockOperationCost +struct MockProportionalCost <: AbstractMockCost proportional_term::Float64 is_time_variant::Bool fuel_cost::Float64 end -MockOperationCost(proportional_term::Float64) = - MockOperationCost(proportional_term, false, 0.0) -MockOperationCost(proportional_term::Float64, is_time_variant::Bool) = - MockOperationCost(proportional_term, is_time_variant, 0.0) +MockProportionalCost(proportional_term::Float64) = + MockProportionalCost(proportional_term, false, 0.0) +MockProportionalCost(proportional_term::Float64, is_time_variant::Bool) = + MockProportionalCost(proportional_term, is_time_variant, 0.0) + +# FIXME mildly awkward that we need both fixed and fuel_cost here, but otherwise +# can't define get_fuel_cost for MockThermalGen. +struct MockOperationalCost <: AbstractMockCost + variable::IS.CostCurve + fixed::Float64 + fuel_cost::Float64 +end + +get_variable(cost::MockOperationalCost) = cost.variable +get_fixed(cost::MockOperationalCost) = cost.fixed +get_fuel_cost(cost::MockOperationalCost) = cost.fuel_cost # Abstract mock device type for testing rejection of abstract types in DeviceModel # Subtypes IS.InfrastructureSystemsComponent so they work with DeviceModel and container keys @@ -48,21 +62,27 @@ get_bustype(b::MockBus) = b.bustype struct MockThermalGen <: AbstractMockGenerator name::String available::Bool - bus::MockBus active_power_limits::NamedTuple{(:min, :max), Tuple{Float64, Float64}} base_power::Float64 - operation_cost::MockOperationCost + operation_cost::AbstractMockCost end # Constructor with default base_power and no operation cost for backward compatibility -MockThermalGen(name, available, bus, limits) = - MockThermalGen(name, available, bus, limits, 100.0, MockOperationCost(0.0)) -MockThermalGen(name, available, bus, limits, base_power) = - MockThermalGen(name, available, bus, limits, base_power, MockOperationCost(0.0)) +MockThermalGen(name, available, limits) = + MockThermalGen(name, available, limits, 100.0, MockProportionalCost(0.0)) +MockThermalGen(name, available, limits, base_power) = + MockThermalGen(name, available, limits, base_power, MockProportionalCost(0.0)) +MockThermalGen(name, available, limits, base_power, operation_cost::IS.CostCurve) = + MockThermalGen( + name, + available, + limits, + base_power, + MockOperationalCost(operation_cost, 0.0, 0.0), + ) get_name(g::MockThermalGen) = g.name get_available(g::MockThermalGen) = g.available -get_bus(g::MockThermalGen) = g.bus IOM.get_active_power_limits(g::MockThermalGen) = g.active_power_limits IOM.get_base_power(g::MockThermalGen) = g.base_power IOM.get_operation_cost(g::MockThermalGen) = g.operation_cost @@ -85,13 +105,11 @@ get_rating(r::MockRenewableGen) = r.rating struct MockLoad <: AbstractMockDevice name::String available::Bool - bus::MockBus max_active_power::Float64 end get_name(l::MockLoad) = l.name get_available(l::MockLoad) = l.available -get_bus(l::MockLoad) = l.bus get_max_active_power(l::MockLoad) = l.max_active_power # Mock Branch @@ -101,13 +119,18 @@ struct MockBranch <: AbstractMockDevice from_bus::MockBus to_bus::MockBus rating::Float64 + r::Float64 end +MockBranch(name, available, from_bus, to_bus, rating) = + MockBranch(name, available, from_bus, to_bus, rating, 0.0) + get_name(b::MockBranch) = b.name get_available(b::MockBranch) = b.available get_from_bus(b::MockBranch) = b.from_bus get_to_bus(b::MockBranch) = b.to_bus get_rate(b::MockBranch) = b.rating +get_r(b::MockBranch) = b.r # Mock component type for use as type parameter in container keys # This replaces PSY.ThermalStandard etc. in tests that don't need real PSY types diff --git a/test/performance/performance_test.jl b/test/performance/performance_test.jl index d063c24..9560435 100644 --- a/test/performance/performance_test.jl +++ b/test/performance/performance_test.jl @@ -109,7 +109,7 @@ try system_to_file = false, initialize_model = true, optimizer_solve_log_print = false, - direct_mode_optimizer = true, + direct_model_optimizer = true, check_numerical_bounds = false, ), DecisionModel( diff --git a/test/test_bilinear_delta_benchmark.jl b/test/test_bilinear_delta_benchmark.jl new file mode 100644 index 0000000..479acc1 --- /dev/null +++ b/test/test_bilinear_delta_benchmark.jl @@ -0,0 +1,296 @@ +using Random +using Dates +using JuMP +using HiGHS +using Ipopt + +using InfrastructureSystems +using InfrastructureOptimizationModels +const IS = InfrastructureSystems +const ISOPT = InfrastructureSystems.Optimization +const IOM = InfrastructureOptimizationModels + +# ----------------- Problem generation + +function _random_convex_pwl_points(n_tranches::Int, rng) + xs = sort(rand(rng, n_tranches - 1)) .* 1.5 + points = [(0.0, 0.0)] + cumulative_cost = 0.0 + prev_x = 0.0 + slope = 0.5 * rand(rng) + for x in xs + cumulative_cost += slope * (x - prev_x) + push!(points, (x, cumulative_cost)) + prev_x = x + slope += 0.5 * rand(rng) + end + cumulative_cost += slope * (1.5 - prev_x) + push!(points, (1.5, cumulative_cost)) + return points +end + +struct NetworkProblem + size::Integer + gen_ids::UnitRange{Integer} + dem_ids::UnitRange{Integer} + edges::Set{Tuple{Integer, Integer}} + demands::Vector{Float64} + conductances::Dict{Tuple{Integer, Integer}, Float64} + cost_functions::Vector{IS.CostCurve{IS.PiecewisePointCurve}} +end +function NetworkProblem(; size, n_cost_segments, seed) + rng = MersenneTwister(seed) + half = div(size, 2) + + edges = Set{Tuple{Integer, Integer}}() + conductances = Dict{Tuple{Integer, Integer}, Float64}() + permutation = shuffle(rng, 1:size) + for i in 1:(size - 1) + i, j = permutation[i], permutation[i + 1] + e = i < j ? (i, j) : (j, i) + push!(edges, e) + conductances[e] = 0.005 + 0.005 * rand(rng) + end + for _ in 1:half + i, j = shuffle(rng, 1:size)[1:2] + e = i < j ? (i, j) : (j, i) + push!(edges, e) + conductances[e] = 0.005 + 0.005 * rand(rng) + end + + cost_functions = Vector{IS.CostCurve{IS.PiecewisePointCurve}}() + for i in 1:half + points = _random_convex_pwl_points(n_cost_segments, rng) + pwl = IS.PiecewiseLinearData(points) + cost_curve = IS.CostCurve(IS.InputOutputCurve(pwl)) + push!(cost_functions, cost_curve) + end + + return NetworkProblem( + size, + 1:half, + (half + 1):size, + edges, + 0.05 .+ 0.1 * rand(rng, half), + conductances, + cost_functions, + ) +end + +# ----------------- Mock infrastructure + +struct MockDeviceFormulation <: IOM.AbstractDeviceFormulation end +struct MockPowerModel <: IS.Optimization.AbstractPowerModel end + +abstract type AbstractMockNetworkNodeType end +struct MockNetworkGenerator <: AbstractMockNetworkNodeType end +struct MockNetworkDemand <: AbstractMockNetworkNodeType end +struct MockNetworkNode <: IS.InfrastructureSystemsComponent + type::AbstractMockNetworkNodeType + id::Integer + adj::Vector{Tuple{Int, Float64}} + current_bounds::NTuple{2, Float64} + cost_function::Union{Nothing, IS.CostCurve{IS.PiecewisePointCurve}} + demand::Float64 +end +MockNetworkNode(id, adj, cost_function::IS.CostCurve) = + MockNetworkNode( + MockNetworkGenerator(), + id, + adj, + (0.0, 1.0), + cost_function, + 0.0, + ) +MockNetworkNode(id, adj, demand::Float64) = + MockNetworkNode( + MockNetworkDemand(), + id, + adj, + (-1.0, 0.0), + nothing, + demand, + ) +IOM.get_name(n::MockNetworkNode) = string(n.id) +IOM.get_base_power(::MockNetworkNode) = 1.0 +get_power_bounds(::MockNetworkNode) = (0.0, 1.5) +get_voltage_bounds(::MockNetworkNode) = (0.8, 1.2) +get_current_bounds(n::MockNetworkNode) = n.current_bounds + +struct MockSystem <: IS.InfrastructureSystemsContainer + nodes::Vector{MockNetworkNode} +end +IOM.get_base_power(::MockSystem) = 1.0 +IOM.stores_time_series_in_memory(::MockSystem) = false +IOM.get_available_components(_, _, sys::MockSystem) = length(sys.nodes) +IOM.calculate_aux_variables!(_, ::MockSystem) = + IOM.RunStatus.SUCCESSFULLY_FINALIZED +IOM.calculate_dual_variables!(_, ::MockSystem, _) = + IOM.RunStatus.SUCCESSFULLY_FINALIZED +get_components(_, sys::MockSystem) = sys.nodes + +# ----------------- Container types + +struct MockKCLConstraint <: ConstraintType end +struct MockPVIConstraint <: ConstraintType end # p = v * i +struct MockVoltageVariable <: VariableType end +struct MockCurrentVariable <: VariableType end + +# ----------------- IOM patches + +IOM.intermediate_set_units_base_system!(::MockSystem, base) = nothing +IOM.intermediate_get_forecast_initial_timestamp(::MockSystem) = DateTime(1970) + +# ----------------- System / container building + +function _build_adj(problem, i) + adj = Vector{Tuple{Int, Float64}}() + for edge in problem.edges + if i in edge + j = i == edge[1] ? edge[2] : edge[1] + push!(adj, (j, problem.conductances[edge])) + end + end + return adj +end + +function build_system(problem) + gen_nodes = [ + MockNetworkNode( + id, + _build_adj(problem, id), + cost_function, + ) + for (id, cost_function) + in zip(problem.gen_ids, problem.cost_functions) + ] + dem_nodes = [ + MockNetworkNode( + id, + _build_adj(problem, id), + demand, + ) + for (id, demand) + in zip(problem.dem_ids, problem.demands) + ] + return MockSystem([gen_nodes; dem_nodes]) +end + +function add_variables!(container, sys) + jump_model = get_jump_model(container) + nodes = get_components(MockNetworkNode, sys) + variable_types = [ActivePowerVariable, MockVoltageVariable, MockCurrentVariable] + bounds_fns = [get_power_bounds, get_voltage_bounds, get_current_bounds] + + for (variable_type, bounds_fn) in zip(variable_types, bounds_fns) + variable = add_variable_container!( + container, + variable_type(), + MockNetworkNode, + get_name.(nodes), + 1:1, + ) + for node in nodes + name = get_name(node) + lower_bound, upper_bound = bounds_fn(node) + variable[name, 1] = JuMP.@variable( + jump_model, + base_name = "$(variable_type)_MockNetworkNode_{$(name), 1}", + lower_bound = lower_bound, upper_bound = upper_bound + ) + end + end + return +end + +function add_constraints!(container, sys) + jump_model = get_jump_model(container) + nodes = get_components(MockNetworkNode, sys) + voltages = IOM.get_variable(container, MockVoltageVariable(), MockNetworkNode) + currents = IOM.get_variable(container, MockCurrentVariable(), MockNetworkNode) + powers = IOM.get_variable(container, ActivePowerVariable(), MockNetworkNode) + + kcl_container = add_constraints_container!( + container, + MockKCLConstraint(), + MockNetworkNode, + get_name.(nodes), + 1:1, + ) + for node in nodes + name = get_name(node) + I, Vi = currents[name, 1], voltages[name, 1] + v_diff = JuMP.AffExpr(0.0) + for (j, conductance) in node.adj + Vj = voltages[string(j), 1] + JuMP.add_to_expression!(v_diff, conductance, Vi) + JuMP.add_to_expression!(v_diff, -conductance, Vj) + end + kcl_container[name, 1] = JuMP.@constraint(jump_model, I == v_diff) + end + + for node in nodes + if node.type isa MockNetworkGenerator + IOM.add_variable_cost_to_objective!( + container, + ActivePowerVariable(), + node, + node.cost_function, + MockDeviceFormulation(), + ) + end + end + + z_container = IOM._add_bilinear!( + container, + MockNetworkNode, + get_name.(nodes), + 1:1, + voltages, + currents, + "foo", + ) + pvi_container = add_constraints_container!( + container, + MockPVIConstraint(), + MockNetworkNode, + get_name.(nodes), + 1:1, + ) + for node in nodes + name = get_name(node) + tgt = node.type isa MockNetworkGenerator ? powers[name, 1] : -node.demand + pvi_container[name, 1] = JuMP.@constraint(jump_model, z_container[name, 1] == tgt) + end +end + +function make_container(sys; optimizer = HiGHS.Optimizer) + container = IOM.OptimizationContainer( + sys, + IOM.Settings( + sys; + horizon = Dates.Hour(1), + resolution = Dates.Hour(1), + optimizer, + ), + JuMP.Model(), + IS.Deterministic, + ) + IOM.set_time_steps!(container, 1:1) + return container +end + +# ----------------- Profiling and comparison + +# ----------------- + +problem = NetworkProblem(; size = 10, n_cost_segments = 3, seed = 0) +sys = build_system(problem) +container = make_container(sys; optimizer = Ipopt.Optimizer) +add_variables!(container, sys) +add_constraints!(container, sys) +network = NetworkModel(MockPowerModel) +init_optimization_container!(container, network, sys) +IOM.update_objective_function!(container) +status = IOM.execute_optimizer!(container, sys) +@show status diff --git a/test/test_piecewise_linear.jl b/test/test_piecewise_linear.jl index 4c77bfb..e036845 100644 --- a/test/test_piecewise_linear.jl +++ b/test/test_piecewise_linear.jl @@ -95,9 +95,9 @@ function setup_pwl_test(; # When fuel_cost is provided, the device's operation_cost must also have it # because get_fuel_cost(device) is called to look up the cost multiplier if isnothing(fuel_cost) - op_cost = MockOperationCost(0.0, false, 0.0) + op_cost = MockProportionalCost(0.0, false, 0.0) else - op_cost = MockOperationCost(0.0, false, fuel_cost) + op_cost = MockProportionalCost(0.0, false, fuel_cost) end device = make_mock_thermal( device_name; diff --git a/test/test_proportional.jl b/test/test_proportional.jl index 2a961ef..4fd2d50 100644 --- a/test/test_proportional.jl +++ b/test/test_proportional.jl @@ -18,9 +18,9 @@ InfrastructureOptimizationModels.objective_function_multiplier( # Interface implementations for mock types -# Non-time-varying proportional_cost: return the proportional_term from MockOperationCost +# Non-time-varying proportional_cost: return the proportional_term from MockProportionalCost InfrastructureOptimizationModels.proportional_cost( - op_cost::MockOperationCost, + op_cost::MockProportionalCost, ::TestProportionalVariable, d::MockThermalGen, ::TestProportionalFormulation, @@ -29,17 +29,17 @@ InfrastructureOptimizationModels.proportional_cost( # Time-varying proportional_cost: same value for all time steps (could vary if needed) InfrastructureOptimizationModels.proportional_cost( ::InfrastructureOptimizationModels.OptimizationContainer, - op_cost::MockOperationCost, + op_cost::MockProportionalCost, ::TestProportionalVariable, d::MockThermalGen, ::TestProportionalFormulation, ::Int, ) = op_cost.proportional_term -# is_time_variant_term: return the is_time_variant flag from MockOperationCost +# is_time_variant_term: return the is_time_variant flag from MockProportionalCost InfrastructureOptimizationModels.is_time_variant_term( ::InfrastructureOptimizationModels.OptimizationContainer, - op_cost::MockOperationCost, + op_cost::MockProportionalCost, ::TestProportionalVariable, ::Type{MockThermalGen}, ::TestProportionalFormulation, @@ -99,7 +99,7 @@ end cost_value = 15.0 device = make_mock_thermal( "gen1"; - operation_cost = MockOperationCost(cost_value, false), + operation_cost = MockProportionalCost(cost_value, false), ) devices = [device] container = setup_proportional_test_container(time_steps, devices) @@ -138,7 +138,7 @@ end time_steps = 1:2 device = make_mock_thermal( "gen1"; - operation_cost = MockOperationCost(0.0, false), + operation_cost = MockProportionalCost(0.0, false), ) devices = [device] container = setup_proportional_test_container(time_steps, devices) @@ -170,11 +170,11 @@ end cost2 = 20.0 device1 = make_mock_thermal( "gen1"; - operation_cost = MockOperationCost(cost1, false), + operation_cost = MockProportionalCost(cost1, false), ) device2 = make_mock_thermal( "gen2"; - operation_cost = MockOperationCost(cost2, false), + operation_cost = MockProportionalCost(cost2, false), ) devices = [device1, device2] container = setup_proportional_test_container(time_steps, devices) @@ -213,7 +213,7 @@ end # is_time_variant = false device = make_mock_thermal( "gen1"; - operation_cost = MockOperationCost(cost_value, false), + operation_cost = MockProportionalCost(cost_value, false), ) devices = [device] container = setup_proportional_test_container(time_steps, devices) @@ -254,7 +254,7 @@ end # is_time_variant = true device = make_mock_thermal( "gen1"; - operation_cost = MockOperationCost(cost_value, true), + operation_cost = MockProportionalCost(cost_value, true), ) devices = [device] container = setup_proportional_test_container(time_steps, devices) @@ -296,11 +296,11 @@ end device_invariant = make_mock_thermal( "gen_inv"; - operation_cost = MockOperationCost(cost_invariant, false), + operation_cost = MockProportionalCost(cost_invariant, false), ) device_variant = make_mock_thermal( "gen_var"; - operation_cost = MockOperationCost(cost_variant, true), + operation_cost = MockProportionalCost(cost_variant, true), ) devices = [device_invariant, device_variant] container = setup_proportional_test_container(time_steps, devices) @@ -356,7 +356,7 @@ end # Zero cost, even if marked as time variant device = make_mock_thermal( "gen1"; - operation_cost = MockOperationCost(0.0, true), + operation_cost = MockProportionalCost(0.0, true), ) devices = [device] container = setup_proportional_test_container(time_steps, devices) diff --git a/test/test_pwl_methods.jl b/test/test_pwl_methods.jl index bf23404..063b8e9 100644 --- a/test/test_pwl_methods.jl +++ b/test/test_pwl_methods.jl @@ -41,7 +41,7 @@ function make_mock_thermal_pwl( fuel_cost = 0.0, ) bus = MockBus("bus1", 1, :PV) - op_cost = MockOperationCost(0.0, false, fuel_cost) + op_cost = MockProportionalCost(0.0, false, fuel_cost) return MockThermalGen( name, true, diff --git a/test/test_settings.jl b/test/test_settings.jl index 115762b..be37a76 100644 --- a/test/test_settings.jl +++ b/test/test_settings.jl @@ -27,7 +27,7 @@ end @test PSI.get_resolution(settings) == Dates.Millisecond(Hour(1)) @test PSI.get_warm_start(settings) == true @test PSI.get_optimizer(settings) === nothing - @test PSI.get_direct_mode_optimizer(settings) == false + @test PSI.get_direct_model_optimizer(settings) == false @test PSI.get_optimizer_solve_log_print(settings) == false @test PSI.get_detailed_optimizer_stats(settings) == false @test PSI.get_calculate_conflict(settings) == false diff --git a/test/verify_mocks.jl b/test/verify_mocks.jl index 3160920..ba5b205 100644 --- a/test/verify_mocks.jl +++ b/test/verify_mocks.jl @@ -22,15 +22,22 @@ get_name(bus) get_number(bus) get_bustype(bus) -# MockOperationCost -MockOperationCost(10.0) -MockOperationCost(10.0, true) -MockOperationCost(10.0, false, 5.0) +# MockProportionalCost +MockProportionalCost(10.0) +MockProportionalCost(10.0, true) +MockProportionalCost(10.0, false, 5.0) # MockThermalGen gen = MockThermalGen("gen1", true, bus, (min = 0.0, max = 100.0)) MockThermalGen("gen2", true, bus, (min = 0.0, max = 100.0), 200.0) -MockThermalGen("gen3", true, bus, (min = 0.0, max = 100.0), 200.0, MockOperationCost(5.0)) +MockThermalGen( + "gen3", + true, + bus, + (min = 0.0, max = 100.0), + 200.0, + MockProportionalCost(5.0), +) get_name(gen) get_available(gen) get_bus(gen)