Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions src/quadratic_approximations/pwmcc_cuts.jl
Original file line number Diff line number Diff line change
Expand Up @@ -85,23 +85,23 @@ function _add_pwmcc_concave_cuts!(

selector_cons = add_constraints_container!(
container,
PiecewiseMcCormickSelectorSum(),
PiecewiseMcCormickSelectorSum,
C,
names,
time_steps;
meta,
)
linking_cons = add_constraints_container!(
container,
PiecewiseMcCormickLinking(),
PiecewiseMcCormickLinking,
C,
names,
time_steps;
meta,
)
interval_lb_cons = add_constraints_container!(
container,
PiecewiseMcCormickIntervalLB(),
PiecewiseMcCormickIntervalLB,
C,
names,
1:K,
Expand All @@ -110,7 +110,7 @@ function _add_pwmcc_concave_cuts!(
)
interval_ub_cons = add_constraints_container!(
container,
PiecewiseMcCormickIntervalUB(),
PiecewiseMcCormickIntervalUB,
C,
names,
1:K,
Expand All @@ -119,23 +119,23 @@ function _add_pwmcc_concave_cuts!(
)
chord_ub_cons = add_constraints_container!(
container,
PiecewiseMcCormickChordUB(),
PiecewiseMcCormickChordUB,
C,
names,
time_steps;
meta,
)
tangent_lb_l_cons = add_constraints_container!(
container,
PiecewiseMcCormickTangentLBL(),
PiecewiseMcCormickTangentLBL,
C,
names,
time_steps;
meta,
)
tangent_lb_r_cons = add_constraints_container!(
container,
PiecewiseMcCormickTangentLBR(),
PiecewiseMcCormickTangentLBR,
C,
names,
time_steps;
Expand Down
53 changes: 53 additions & 0 deletions test/test_hybs_approximations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -521,4 +521,57 @@ end
@test n_bin_hybs < n_bin_bin2
end
end

@testset "HybS with McCormick envelope tightens bounds" begin
# Compare HybSConfig(..., false) vs HybSConfig(..., true) at a fixed point
x0, y0 = 0.4, 0.7
true_product = x0 * y0

results = Dict{Symbol, NTuple{2, Float64}}() # (min_z, max_z) for each config
for (label, add_mc) in [(:no_mc, false), (:with_mc, true)]
z_vals = Float64[]
for sense in [JuMP.MIN_SENSE, JuMP.MAX_SENSE]
setup = _setup_bilinear_test(["dev1"], 1:1)
JuMP.fix(setup.x_var_container["dev1", 1], x0; force = true)
JuMP.fix(setup.y_var_container["dev1", 1], y0; force = true)

IOM._add_bilinear_approx!(
IOM.HybSConfig(IOM.SawtoothQuadConfig(2), 2, add_mc),
setup.container,
MockThermalGen,
["dev1"],
1:1,
setup.x_var_container,
setup.y_var_container,
0.0,
1.0,
0.0,
1.0,
HYBS_META,
)
expr_container = IOM.get_expression(
setup.container,
IOM.BilinearProductExpression,
MockThermalGen,
HYBS_META,
)
z_expr = expr_container["dev1", 1]

JuMP.@objective(setup.jump_model, sense, z_expr)
JuMP.set_optimizer(setup.jump_model, HiGHS.Optimizer)
JuMP.set_silent(setup.jump_model)
JuMP.optimize!(setup.jump_model)
@test JuMP.termination_status(setup.jump_model) == JuMP.OPTIMAL
push!(z_vals, JuMP.objective_value(setup.jump_model))
end
results[label] = (z_vals[1], z_vals[2]) # (min_z, max_z)
end
# McCormick version should have gap at least as tight
no_mc_gap = results[:no_mc][2] - results[:no_mc][1]
with_mc_gap = results[:with_mc][2] - results[:with_mc][1]
@test with_mc_gap <= no_mc_gap + 1e-6
# Both should bracket the true product
@test results[:with_mc][1] <= true_product + 1e-6
@test results[:with_mc][2] >= true_product - 1e-6
end
end
41 changes: 41 additions & 0 deletions test/test_jump_utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,22 @@ struct MockVariableType <: ISOPT.VariableType end
@test isnan(IOM.jump_value(y))
end

@testset "jump_value with solved model" begin
model = JuMP.Model(HiGHS.Optimizer)
JuMP.set_silent(model)
@variable(model, x >= 5.0)
@objective(model, Min, x)
JuMP.optimize!(model)
@test JuMP.termination_status(model) == JuMP.OPTIMAL
@test IOM.jump_value(x) ≈ 5.0 atol = 1e-6
end

@testset "add_proportional_to_jump_expression! Float64×Float64" begin
expr = JuMP.AffExpr(1.0)
IOM.add_proportional_to_jump_expression!(expr, 3.0, 4.0)
@test JuMP.constant(expr) ≈ 13.0 # 1.0 + 3.0*4.0
end

@testset "jump_fixed_value" begin
model = JuMP.Model()

Expand Down Expand Up @@ -266,4 +282,29 @@ struct MockVariableType <: ISOPT.VariableType end
@test :value in propertynames(df)
@test nrow(df) == 12 # 2 * 2 * 3
end

@testset "_get_piecewise_pointcurve_per_system_unit DEVICE_BASE" begin
# Points in device base units: x coordinates should be scaled by
# device_base / system_base, y coordinates unchanged
points = [(x = 0.0, y = 0.0), (x = 1.0, y = 10.0), (x = 2.0, y = 30.0)]
pwl_data = IS.PiecewiseLinearData(points)
system_base = 100.0
device_base = 50.0

result = IOM._get_piecewise_pointcurve_per_system_unit(
pwl_data,
Val(IS.UnitSystem.DEVICE_BASE),
system_base,
device_base,
)
result_points = result.points
ratio = device_base / system_base # 0.5
@test result_points[1].x ≈ 0.0 * ratio
@test result_points[2].x ≈ 1.0 * ratio
@test result_points[3].x ≈ 2.0 * ratio
# y-coordinates unchanged
@test result_points[1].y ≈ 0.0
@test result_points[2].y ≈ 10.0
@test result_points[3].y ≈ 30.0
end
end
61 changes: 61 additions & 0 deletions test/test_optimization_container.jl
Original file line number Diff line number Diff line change
Expand Up @@ -151,4 +151,65 @@ struct MockExpressionType <: ISOPT.ExpressionType end
expr_key = PSI.ExpressionKey(MockExpressionType, MockComponentType)
@test haskey(PSI.get_expressions(container), expr_key)
end

@testset "_assign_container! throws on duplicate key" begin
mock_sys = MockSystem(100.0)
settings = PSI.Settings(
mock_sys;
horizon = Dates.Hour(24),
resolution = Dates.Hour(1),
time_series_cache_size = 0,
)
container = PSI.OptimizationContainer(
mock_sys,
settings,
nothing,
MockDeterministic,
)
PSI.set_time_steps!(container, 1:24)

# First assignment succeeds
PSI.add_variable_container!(
container,
PSI.ActivePowerVariable,
MockComponentType,
["gen1"],
1:24,
)

# Second assignment with same key should throw.
# Suppress the @error log to avoid tripping the framework's zero-error assertion.
@test_throws IS.InvalidValue Logging.with_logger(Logging.NullLogger()) do
PSI.add_variable_container!(
container,
PSI.ActivePowerVariable,
MockComponentType,
["gen1"],
1:24,
)
end
end

@testset "_get_entry throws on missing key" begin
mock_sys = MockSystem(100.0)
settings = PSI.Settings(
mock_sys;
horizon = Dates.Hour(24),
resolution = Dates.Hour(1),
time_series_cache_size = 0,
)
container = PSI.OptimizationContainer(
mock_sys,
settings,
nothing,
MockDeterministic,
)

# Looking up a variable that was never added should throw
@test_throws IS.InvalidValue PSI.get_variable(
container,
PSI.ActivePowerVariable,
MockComponentType,
)
end
end
127 changes: 127 additions & 0 deletions test/test_piecewise_linear.jl
Original file line number Diff line number Diff line change
Expand Up @@ -798,6 +798,72 @@ end
@test JuMP.coefficient(invariant, var_y800) ≈ 2000.0 atol = 1e-10
end

@testset "FuelCurve{PiecewisePointCurve} time-variant fuel cost goes to variant objective" begin
fuel_points = [
(x = 0.0, y = 0.0),
(x = 50.0, y = 400.0),
(x = 100.0, y = 900.0),
]
time_steps = 1:2
fuel_prices = [3.0, 7.0]

# Create container + device. We pass a scalar fuel_cost for the operation cost mock
# but use a TimeSeriesKey in the FuelCurve itself.
op_cost = MockOperationCost(0.0, false, 5.0)
device = make_mock_thermal(
"gen_tv"; base_power = 100.0, operation_cost = op_cost,
)
container = setup_pwl_container_with_variables(time_steps, device)

# Pre-populate FuelCostParameter with time-varying fuel prices
add_test_parameter!(
container,
IOM.FuelCostParameter,
MockThermalGen,
["gen_tv"],
time_steps,
reshape(Float64.(fuel_prices), 1, :),
)

# Build FuelCurve with a TimeSeriesKey as fuel_cost to trigger is_time_variant
ts_key = IS.StaticTimeSeriesKey(
IS.SingleTimeSeries,
"fuel_cost",
Dates.DateTime(2024, 1, 1),
Dates.Hour(1),
length(time_steps),
Dict{String, Any}(),
)
fuel_curve = IS.FuelCurve(
IS.InputOutputCurve(IS.PiecewiseLinearData(fuel_points)),
IS.UnitSystem.NATURAL_UNITS,
ts_key,
)

IOM.add_variable_cost_to_objective!(
container,
TestPWLVariable,
device,
fuel_curve,
TestPWLFormulation,
)

# Cost should be in the VARIANT objective, not invariant
obj = IOM.get_objective_expression(container)
variant = IOM.get_variant_terms(obj)
invariant = IOM.get_invariant_terms(obj)

pwl_var_container = IOM.get_variable(
container, IOM.PiecewiseLinearCostVariable, MockThermalGen,
)

# For t=1: y=400 point, fuel_price=3.0, dt=1.0 → cost coef = 400 * 3.0 = 1200
var_y400_t1 = pwl_var_container["gen_tv", 2, 1]
@test JuMP.coefficient(variant, var_y400_t1) ≈ 400.0 * fuel_prices[1] atol = 1e-10
# Invariant should NOT contain this term
@test JuMP.coefficient(invariant, var_y400_t1) ≈ 0.0 atol = 1e-10
end

@testset "FuelCurve{PiecewiseIncrementalCurve} converts and produces correct objective" begin
# FuelCurve with incremental (marginal heat rate) data
# x_coords: [0, 50, 100] MW, slopes: [8, 10] MMBTU/MWh
Expand Down Expand Up @@ -893,4 +959,65 @@ end
con_set = JuMP.constraint_object(linking_con).set
@test con_set.value ≈ -P_min
end

@testset "_add_pwl_constraint_standard! with must_run=true forces bin=1.0" begin
P_min = 20.0
must_run_points =
[(x = 20.0, y = 0.0), (x = 60.0, y = 200.0), (x = 100.0, y = 800.0)]
(; container, device, break_points, power_var) =
setup_pwl_constraint_test(; time_steps = 1:1, points = must_run_points)

IOM._add_pwl_constraint_standard!(
container,
device,
break_points,
IOM.SOSStatusVariable.NO_VARIABLE,
1,
power_var,
true, # must_run
)

# Normalization constraint RHS should be 1.0 (bin forced)
norm_con = IOM.get_constraint(
container,
IOM.PiecewiseLinearCostNormalizationConstraint,
MockThermalGen,
)[
"gen1",
1,
]
norm_set = JuMP.constraint_object(norm_con).set
@test norm_set.value ≈ 1.0
end

@testset "_get_sos_value returns NO_VARIABLE when skip_proportional_cost" begin
# Temporarily override skip_proportional_cost for our mock type
IOM.skip_proportional_cost(::MockThermalGen) = true
try
setup = setup_pwl_test()
result = IOM._get_sos_value(
setup.container, TestPWLFormulation, setup.device,
)
@test result == IOM.SOSStatusVariable.NO_VARIABLE
finally
IOM.skip_proportional_cost(::MockThermalGen) = false
end
end

@testset "_get_sos_value returns PARAMETER when OnStatusParameter exists" begin
(; container, device) = setup_pwl_test(; time_steps = 1:1)

# Add OnStatusParameter container
add_test_parameter!(
container,
IOM.OnStatusParameter,
MockThermalGen,
["gen1"],
1:1,
fill(1.0, 1, 1),
)

result = IOM._get_sos_value(container, TestPWLFormulation, device)
@test result == IOM.SOSStatusVariable.PARAMETER
end
end
Loading
Loading