diff --git a/scripts/generate_lcov.jl b/scripts/generate_lcov.jl new file mode 100644 index 0000000..d16dd23 --- /dev/null +++ b/scripts/generate_lcov.jl @@ -0,0 +1,18 @@ +#!/usr/bin/env julia +# +# Generate lcov.info from existing .cov files for Coverage Gutters. +# Run this AFTER running tests with coverage. +# +# Usage: +# 1. Run tests with coverage: julia --project=. -e 'using TestEnv; TestEnv.activate(); include("test/load_tests.jl"); InfrastructureOptimizationModelsTests.run_tests()' +# 2. Generate lcov: julia --project=. -e 'using TestEnv; TestEnv.activate(); include("scripts/generate_lcov.jl")' + +using CoverageTools +using Coverage + +const PROJECT_ROOT = dirname(@__DIR__) + +coverage = CoverageTools.process_folder(joinpath(PROJECT_ROOT, "src")) +LCOV.writefile(joinpath(PROJECT_ROOT, "lcov.info"), coverage) + +@info "Wrote $(joinpath(PROJECT_ROOT, "lcov.info"))" diff --git a/scripts/test_with_coverage.sh b/scripts/test_with_coverage.sh new file mode 100755 index 0000000..e973adf --- /dev/null +++ b/scripts/test_with_coverage.sh @@ -0,0 +1,21 @@ +#!/bin/zsh +# +# Run tests with coverage and generate lcov.info for Coverage Gutters. +# Usage: ./scripts/test_with_coverage.sh + +set -euo pipefail +cd "$(dirname "$0")/.." + +echo "==> Running tests with coverage..." +julia --project=. --code-coverage -e ' + using TestEnv; TestEnv.activate() + include("test/runtests.jl") +' + +echo "==> Generating lcov.info..." +julia --project=. -e ' + using TestEnv; TestEnv.activate() + include("scripts/generate_lcov.jl") +' + +echo "==> Done. lcov.info written to $(pwd)/lcov.info" diff --git a/src/ac_transmission_models/branch_constructor.jl b/src/ac_transmission_models/branch_constructor.jl index 568b072..9ce0b34 100644 --- a/src/ac_transmission_models/branch_constructor.jl +++ b/src/ac_transmission_models/branch_constructor.jl @@ -583,7 +583,7 @@ function construct_device!( device_model, network_model, ) - add_feedforward_arguments!(container, devicemodel, devices) + add_feedforward_arguments!(container, device_model, devices) return end diff --git a/src/energy_storage_models/storage_models.jl b/src/energy_storage_models/storage_models.jl index 2630d76..0eed42f 100644 --- a/src/energy_storage_models/storage_models.jl +++ b/src/energy_storage_models/storage_models.jl @@ -1415,7 +1415,7 @@ function add_cycling_charge_without_reserves!( powerin_var = get_variable(container, ActivePowerInVariable, V) slack_var = get_variable(container, StorageChargeCyclingSlackVariable, V) - constraint = add_constraints_container!(container, StorageCyclingCharge, V, names) + constraint = add_constraints_container!(container, StorageCyclingCharge, V, names, time_steps[end:end]) for d in devices name = PSY.get_name(d) @@ -1435,7 +1435,6 @@ function add_cycling_charge_without_reserves!( return end -# no test coverage function add_cycling_charge_with_reserves!( container::OptimizationContainer, devices::IS.FlattenIteratorWrapper{V}, @@ -1461,7 +1460,7 @@ function add_cycling_charge_with_reserves!( PSY.get_conversion_factor(d) cycle_count = PSY.get_cycle_limits(d) efficiency = PSY.get_efficiency(d) - constraint[name, time_steps[end]] = JuMP.@constraint( + constraint[name] = JuMP.@constraint( get_jump_model(container), sum(( (powerin_var[name, t] + r_dn_ch[name, t]) * @@ -1473,7 +1472,6 @@ function add_cycling_charge_with_reserves!( return end -# no test coverage function add_constraints!( container::OptimizationContainer, ::Type{StorageCyclingCharge}, @@ -1514,7 +1512,7 @@ function add_cycling_discharge_without_reserves!( PSY.get_conversion_factor(d) cycle_count = PSY.get_cycle_limits(d) efficiency = PSY.get_efficiency(d) - constraint[name, time_steps[end]] = JuMP.@constraint( + constraint[name] = JuMP.@constraint( get_jump_model(container), sum( (powerout_var[name, t] / efficiency.out) * fraction_of_hour for @@ -1525,7 +1523,6 @@ function add_cycling_discharge_without_reserves!( return end -# no test coverage function add_cycling_discharge_with_reserves!( container::OptimizationContainer, devices::IS.FlattenIteratorWrapper{V}, @@ -1551,7 +1548,7 @@ function add_cycling_discharge_with_reserves!( PSY.get_conversion_factor(d) cycle_count = PSY.get_cycle_limits(d) efficiency = PSY.get_efficiency(d) - constraint[name, time_steps[end]] = JuMP.@constraint( + constraint[name] = JuMP.@constraint( get_jump_model(container), sum( ((powerout_var[name, t] + r_up_ds[name, t]) / efficiency.out) * @@ -1562,7 +1559,6 @@ function add_cycling_discharge_with_reserves!( return end -# no test coverage function add_constraints!( container::OptimizationContainer, ::Type{StorageCyclingDischarge}, diff --git a/src/initial_conditions/update_initial_conditions.jl b/src/initial_conditions/update_initial_conditions.jl index 159be64..74c0bab 100644 --- a/src/initial_conditions/update_initial_conditions.jl +++ b/src/initial_conditions/update_initial_conditions.jl @@ -2,18 +2,18 @@ # IC type -> variable/aux-variable type mapping (dispatch-based) ################################################################################# -_ic_variable_type(::Type{DevicePower}) = ActivePowerVariable() -_ic_variable_type(::Type{DeviceStatus}) = OnVariable() -_ic_variable_type(::Type{DeviceAboveMinPower}) = PowerAboveMinimumVariable() -_ic_variable_type(::Type{InitialTimeDurationOn}) = TimeDurationOn() -_ic_variable_type(::Type{InitialTimeDurationOff}) = TimeDurationOff() -_ic_variable_type(::Type{InitialEnergyLevel}) = EnergyVariable() +_ic_variable_type(::Type{DevicePower}) = ActivePowerVariable +_ic_variable_type(::Type{DeviceStatus}) = OnVariable +_ic_variable_type(::Type{DeviceAboveMinPower}) = PowerAboveMinimumVariable +_ic_variable_type(::Type{InitialTimeDurationOn}) = TimeDurationOn +_ic_variable_type(::Type{InitialTimeDurationOff}) = TimeDurationOff +_ic_variable_type(::Type{InitialEnergyLevel}) = EnergyVariable # Dispatch to the right container getter based on variable vs aux variable type # FIXME we should add something like this to the API. -_get_from_container(source, var_type::VariableType, comp_type) = +_get_from_container(source, var_type::Type{<:VariableType}, comp_type) = get_variable(source, var_type, comp_type) -_get_from_container(source, var_type::AuxVariableType, comp_type) = +_get_from_container(source, var_type::Type{<:AuxVariableType}, comp_type) = get_aux_variable(source, var_type, comp_type) ################################################################################# diff --git a/src/twoterminal_hvdc_models/branch_constructor.jl b/src/twoterminal_hvdc_models/branch_constructor.jl index 60aab5f..f13114e 100644 --- a/src/twoterminal_hvdc_models/branch_constructor.jl +++ b/src/twoterminal_hvdc_models/branch_constructor.jl @@ -565,7 +565,7 @@ function construct_device!( device_model, network_model, ) - add_feedforward_arguments!(container, devicemodel, devices) + add_feedforward_arguments!(container, device_model, devices) return end diff --git a/test/Project.toml b/test/Project.toml index 9445668..05d97e1 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,6 +1,8 @@ [deps] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" +Coverage = "a2441757-f6aa-5fb2-8edb-039e3f45d037" +CoverageTools = "c36e975a-824b-4404-a568-ef97ca766997" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" DataFramesMeta = "1313f7d8-7da2-5740-9ea0-a2ca25f37964" DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" diff --git a/test/test_core_internals.jl b/test/test_core_internals.jl new file mode 100644 index 0000000..9a6039b --- /dev/null +++ b/test/test_core_internals.jl @@ -0,0 +1,105 @@ +############################################################################### +# Tests for core type definitions, interface fallbacks, and helper methods +############################################################################### + +@testset "Network formulation capabilities" begin + # supports_branch_filtering + @test POM.supports_branch_filtering(PTDFPowerModel) == true + @test POM.supports_branch_filtering(POM.SecurityConstrainedPTDFPowerModel) == true + + # ignores_branch_filtering + @test POM.ignores_branch_filtering(CopperPlatePowerModel) == true + @test POM.ignores_branch_filtering(AreaBalancePowerModel) == true + + # requires_all_branch_models + @test POM.requires_all_branch_models(PTDFPowerModel) == false + @test POM.requires_all_branch_models(CopperPlatePowerModel) == false + @test POM.requires_all_branch_models(AreaBalancePowerModel) == false +end + +@testset "Expression type methods" begin + # should_write_resulting_value + @test IOM.should_write_resulting_value(POM.InterfaceTotalFlow) == true + @test IOM.should_write_resulting_value(POM.PTDFBranchFlow) == true + @test IOM.should_write_resulting_value(POM.HydroServedReserveUpExpression) == true + @test IOM.should_write_resulting_value(POM.HydroServedReserveDownExpression) == true + @test IOM.should_write_resulting_value(POM.TotalHydroFlowRateReservoirOutgoing) == true + @test IOM.should_write_resulting_value(POM.TotalHydroFlowRateTurbineOutgoing) == true + + # convert_output_to_natural_units + @test IOM.convert_output_to_natural_units(POM.InterfaceTotalFlow) == true + @test IOM.convert_output_to_natural_units(POM.PTDFBranchFlow) == true +end + +@testset "Auxiliary variable type methods" begin + # convert_output_to_natural_units + @test IOM.convert_output_to_natural_units(POM.PowerOutput) == true + @test IOM.convert_output_to_natural_units(POM.PowerFlowBranchActivePowerFromTo) == true + @test IOM.convert_output_to_natural_units(POM.PowerFlowBranchActivePowerToFrom) == true + @test IOM.convert_output_to_natural_units(POM.PowerFlowBranchReactivePowerFromTo) == + true + @test IOM.convert_output_to_natural_units(POM.PowerFlowBranchReactivePowerToFrom) == + true + @test IOM.convert_output_to_natural_units(POM.PowerFlowBranchActivePowerLoss) == true + + # is_from_power_flow + @test IOM.is_from_power_flow(POM.PowerFlowVoltageAngle) == true + @test IOM.is_from_power_flow(POM.PowerFlowVoltageMagnitude) == true + @test IOM.is_from_power_flow(POM.PowerFlowBranchActivePowerFromTo) == true + @test IOM.is_from_power_flow(POM.PowerFlowLossFactors) == true + @test IOM.is_from_power_flow(POM.PowerFlowVoltageStabilityFactors) == true + @test IOM.is_from_power_flow(POM.TimeDurationOn) == false + @test IOM.is_from_power_flow(POM.PowerOutput) == false +end + +@testset "Parameter type methods" begin + # convert_output_to_natural_units for parameters + @test IOM.convert_output_to_natural_units(POM.ActivePowerTimeSeriesParameter) == true + @test IOM.convert_output_to_natural_units(POM.ReactivePowerTimeSeriesParameter) == true + @test IOM.convert_output_to_natural_units(POM.RequirementTimeSeriesParameter) == true + @test IOM.convert_output_to_natural_units(POM.DynamicBranchRatingTimeSeriesParameter) == + true + @test IOM.convert_output_to_natural_units( + POM.PostContingencyDynamicBranchRatingTimeSeriesParameter, + ) == true + @test IOM.convert_output_to_natural_units(POM.UpperBoundValueParameter) == true + @test IOM.convert_output_to_natural_units(POM.LowerBoundValueParameter) == true + @test IOM.convert_output_to_natural_units(POM.EnergyLimitParameter) == true + @test IOM.convert_output_to_natural_units(POM.EnergyTargetParameter) == true + @test IOM.convert_output_to_natural_units(POM.ReservoirLimitParameter) == true + @test IOM.convert_output_to_natural_units(POM.ReservoirTargetParameter) == true + @test IOM.convert_output_to_natural_units(POM.EnergyTargetTimeSeriesParameter) == true + @test IOM.convert_output_to_natural_units(POM.EnergyBudgetTimeSeriesParameter) == true + @test IOM.convert_output_to_natural_units(POM.InflowTimeSeriesParameter) == false + @test IOM.convert_output_to_natural_units(POM.OutflowTimeSeriesParameter) == false + + # should_write_resulting_value for parameters + @test IOM.should_write_resulting_value(POM.AvailableStatusParameter) == true + @test IOM.should_write_resulting_value(POM.ActivePowerOffsetParameter) == true + @test IOM.should_write_resulting_value(POM.ReactivePowerOffsetParameter) == true + @test IOM.should_write_resulting_value(POM.AvailableStatusChangeCountdownParameter) == + true +end + +@testset "Default interface methods" begin + # get_multiplier_value defaults for OCC parameters + gen = first( + PSY.get_components(PSY.ThermalStandard, PSB.build_system(PSITestSystems, "c_sys5")), + ) + @test POM.get_multiplier_value( + IOM.StartupCostParameter, + gen, + POM.ThermalStandardDispatch, + ) == 1.0 + @test POM.get_multiplier_value( + IOM.ShutdownCostParameter, + gen, + POM.ThermalStandardDispatch, + ) == 1.0 + + # get_default_on_variable / get_default_on_parameter + @test POM.get_default_on_variable(gen) isa POM.OnVariable + @test POM.get_default_on_parameter(gen) isa IOM.OnStatusParameter +end + + diff --git a/test/test_device_branch_constructors.jl b/test/test_device_branch_constructors.jl index 6e9d3d5..8eb8470 100644 --- a/test/test_device_branch_constructors.jl +++ b/test/test_device_branch_constructors.jl @@ -897,3 +897,85 @@ end IOM.ModelBuildStatus.BUILT @test solve!(model_ac) == IOM.RunStatus.SUCCESSFULLY_FINALIZED end + +@testset "StaticBranchBounds with CopperPlatePowerModel" begin + system = PSB.build_system(PSITestSystems, "c_sys5") + template = OperationsProblemTemplate(CopperPlatePowerModel) + set_device_model!(template, ThermalStandard, ThermalBasicDispatch) + set_device_model!(template, PowerLoad, StaticPowerLoad) + set_device_model!(template, Line, StaticBranchBounds) + set_device_model!(template, Transformer2W, StaticBranchBounds) + set_device_model!(template, TapTransformer, StaticBranchBounds) + model = DecisionModel(template, system; optimizer = HiGHS_optimizer) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT + @test solve!(model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED +end + +@testset "StaticBranchUnbounded with PTDFPowerModel" begin + system = PSB.build_system(PSITestSystems, "c_sys5") + template = get_thermal_dispatch_template_network( + NetworkModel(PTDFPowerModel; PTDF_matrix = PTDF(system)), + ) + set_device_model!(template, Line, StaticBranchUnbounded) + model = DecisionModel(template, system; optimizer = HiGHS_optimizer) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT + @test solve!(model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED +end + +@testset "StaticBranch with AreaBalancePowerModel" begin + c_sys = PSB.build_system(PSISystems, "two_area_pjm_DA") + transform_single_time_series!(c_sys, Hour(24), Hour(1)) + template = get_thermal_dispatch_template_network(NetworkModel(AreaBalancePowerModel)) + set_device_model!(template, AreaInterchange, StaticBranch) + model = + DecisionModel(template, c_sys; resolution = Hour(1), optimizer = HiGHS_optimizer) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT + @test solve!(model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED +end + +############################################### +##### SLACK VARIABLE TESTS #################### +############################################### + +@testset "StaticBranch with slacks on PTDF model" begin + system = PSB.build_system(PSITestSystems, "c_sys5") + template = get_thermal_dispatch_template_network( + NetworkModel(PTDFPowerModel; PTDF_matrix = PTDF(system)), + ) + set_device_model!(template, DeviceModel(Line, StaticBranch; use_slacks = true)) + model = DecisionModel(template, system; optimizer = HiGHS_optimizer) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT + + @test !check_variable_bounded(model, FlowActivePowerSlackUpperBound, Line) + @test !check_variable_bounded(model, FlowActivePowerSlackLowerBound, Line) + + @test solve!(model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED +end + +@testset "StaticBranchBounds with CopperPlate no-op" begin + system = PSB.build_system(PSITestSystems, "c_sys5") + template = OperationsProblemTemplate(CopperPlatePowerModel) + set_device_model!(template, ThermalStandard, ThermalBasicDispatch) + set_device_model!(template, PowerLoad, StaticPowerLoad) + set_device_model!(template, Line, StaticBranchBounds) + model = DecisionModel(template, system; optimizer = HiGHS_optimizer) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT + @test solve!(model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED +end + +@testset "StaticBranchUnbounded with CopperPlatePowerModel" begin + system = PSB.build_system(PSITestSystems, "c_sys5") + template = OperationsProblemTemplate(CopperPlatePowerModel) + set_device_model!(template, ThermalStandard, ThermalBasicDispatch) + set_device_model!(template, PowerLoad, StaticPowerLoad) + set_device_model!(template, Line, StaticBranchUnbounded) + model = DecisionModel(template, system; optimizer = HiGHS_optimizer) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT + @test solve!(model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED +end diff --git a/test/test_device_hvdc.jl b/test/test_device_hvdc.jl index 2b158b3..b689160 100644 --- a/test/test_device_hvdc.jl +++ b/test/test_device_hvdc.jl @@ -127,3 +127,26 @@ end IOM.ModelBuildStatus.BUILT @test solve!(model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED end + +@testset "HVDC TwoTerminal Dispatch with PTDFPowerModel" begin + sys = PSB.build_system(PSITestSystems, "c_sys14_dc") + template = get_thermal_dispatch_template_network( + NetworkModel(PTDFPowerModel; PTDF_matrix = PTDF(sys)), + ) + set_device_model!(template, TwoTerminalGenericHVDCLine, HVDCTwoTerminalDispatch) + model = DecisionModel(template, sys; optimizer = HiGHS_optimizer) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT +end + +@testset "HVDC TwoTerminal Unbounded with CopperPlatePowerModel" begin + sys = PSB.build_system(PSITestSystems, "c_sys14_dc") + template = OperationsProblemTemplate(CopperPlatePowerModel) + set_device_model!(template, ThermalStandard, ThermalBasicDispatch) + set_device_model!(template, PowerLoad, StaticPowerLoad) + set_device_model!(template, TwoTerminalGenericHVDCLine, HVDCTwoTerminalUnbounded) + model = DecisionModel(template, sys; optimizer = HiGHS_optimizer) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT + @test solve!(model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED +end diff --git a/test/test_device_hydro_constructors.jl b/test/test_device_hydro_constructors.jl index 2f918fa..b6bcd63 100644 --- a/test/test_device_hydro_constructors.jl +++ b/test/test_device_hydro_constructors.jl @@ -912,3 +912,40 @@ end @test build!(model; output_dir = mktempdir()) == ModelBuildStatus.BUILT @test solve!(model) == IS.Simulation.RunStatus.SUCCESSFULLY_FINALIZED end + +@testset "HydroReservoir with Energy Target" begin + sys = PSB.build_system(PSITestSystems, "c_sys5_hy_turbine_energy") + template = OperationsProblemTemplate(DCPPowerModel) + set_device_model!(template, ThermalStandard, ThermalBasicDispatch) + set_device_model!(template, PowerLoad, StaticPowerLoad) + set_device_model!(template, Line, StaticBranch) + set_device_model!(template, HydroTurbine, HydroTurbineEnergyDispatch) + set_device_model!( + template, + DeviceModel( + HydroReservoir, + HydroEnergyModelReservoir; + attributes = Dict{String, Any}( + "energy_target" => true, + "hydro_budget" => false, + ), + ), + ) + model = DecisionModel(template, sys; optimizer = HiGHS_optimizer) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + ModelBuildStatus.BUILT + @test solve!(model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED +end + +@testset "HydroReservoir with spillage formulation" begin + sys = PSB.build_system(PSITestSystems, "c_sys5_hyd") + template = OperationsProblemTemplate(DCPPowerModel) + set_device_model!(template, ThermalStandard, ThermalBasicDispatch) + set_device_model!(template, PowerLoad, StaticPowerLoad) + set_device_model!(template, Line, StaticBranch) + set_device_model!(template, HydroDispatch, HydroDispatchRunOfRiver) + ED = DecisionModel(template, sys; optimizer = HiGHS_optimizer) + @test build!(ED; output_dir = mktempdir(; cleanup = true)) == + ModelBuildStatus.BUILT + @test solve!(ED) == IOM.RunStatus.SUCCESSFULLY_FINALIZED +end diff --git a/test/test_device_load_constructors.jl b/test/test_device_load_constructors.jl index 4fa3124..b639461 100644 --- a/test/test_device_load_constructors.jl +++ b/test/test_device_load_constructors.jl @@ -240,6 +240,20 @@ end end end +@testset "AreaInterchange with PTDFPowerModel" begin + c_sys = PSB.build_system(PSISystems, "two_area_pjm_DA") + transform_single_time_series!(c_sys, Hour(24), Hour(1)) + template = get_thermal_dispatch_template_network( + NetworkModel(PTDFPowerModel; PTDF_matrix = PTDF(c_sys)), + ) + set_device_model!(template, AreaInterchange, StaticBranch) + ps_model = + DecisionModel(template, c_sys; resolution = Hour(1), optimizer = HiGHS_optimizer) + @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT + @test solve!(ps_model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED +end + @testset "PowerLoadShift with NonAnticipativityConstraint" begin c_sys5_il = PSB.build_system(PSITestSystems, "c_sys5_il"; add_single_time_series = true) diff --git a/test/test_device_thermal_generation_constructors.jl b/test/test_device_thermal_generation_constructors.jl index c784088..1a3398a 100644 --- a/test/test_device_thermal_generation_constructors.jl +++ b/test/test_device_thermal_generation_constructors.jl @@ -1501,3 +1501,58 @@ end @test isapprox(p_steam3[1], x_last) # max @test isapprox(cost_steam3[1], y_last) # last cost end + +############################################ +###### COVERAGE: THERMAL FORMULATIONS ###### +############################################ + +@testset "ThermalStandardDispatch with ramp constraints" begin + c_sys5 = PSB.build_system(PSITestSystems, "c_sys5") + template = OperationsProblemTemplate(CopperPlatePowerModel) + set_device_model!(template, ThermalStandard, ThermalStandardDispatch) + set_device_model!(template, PowerLoad, StaticPowerLoad) + model = DecisionModel(template, c_sys5; optimizer = HiGHS_optimizer) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT + @test solve!(model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED +end + +@testset "ThermalCompactDispatch formulation" begin + c_sys5 = PSB.build_system(PSITestSystems, "c_sys5_uc") + template = OperationsProblemTemplate(CopperPlatePowerModel) + set_device_model!(template, ThermalStandard, ThermalCompactDispatch) + set_device_model!(template, PowerLoad, StaticPowerLoad) + model = DecisionModel(template, c_sys5; optimizer = HiGHS_optimizer) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT + @test solve!(model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED +end + +@testset "ThermalStandard with PTDF network" begin + c_sys5 = PSB.build_system(PSITestSystems, "c_sys5") + template = get_thermal_dispatch_template_network( + NetworkModel(PTDFPowerModel; PTDF_matrix = PTDF(c_sys5)), + ) + set_device_model!(template, ThermalStandard, ThermalStandardDispatch) + model = DecisionModel(template, c_sys5; optimizer = HiGHS_optimizer) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT + @test solve!(model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED +end + +@testset "MarketBidCost with ThermalStandard UC" begin + c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_uc") + add_mbc!(c_sys5_uc, make_selector(ThermalStandard); incremental = true) + + template = OperationsProblemTemplate(NetworkModel(CopperPlatePowerModel)) + set_device_model!(template, ThermalStandard, ThermalBasicUnitCommitment) + set_device_model!(template, PowerLoad, StaticPowerLoad) + + model = DecisionModel( + template, c_sys5_uc; + optimizer = HiGHS_optimizer, + initialize_model = false, + ) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT +end diff --git a/test/test_emulation_model.jl b/test/test_emulation_model.jl new file mode 100644 index 0000000..87a82be --- /dev/null +++ b/test/test_emulation_model.jl @@ -0,0 +1,62 @@ +"""Helper: build c_sys5 with SingleTimeSeries attached (required for EmulationModel).""" +function _build_emulation_system() + sys = PSB.build_system(PSITestSystems, "c_sys5") + init_time = DateTime("2024-01-01") + for load in get_components(PowerLoad, sys) + tstamps = collect(range(init_time; length = 24, step = Hour(1))) + data = TimeArray(tstamps, fill(get_active_power(load), 24)) + ts = SingleTimeSeries(; name = "max_active_power", data = data) + add_time_series!(sys, load, ts) + end + return sys +end + +@testset "EmulationModel Build" begin + sys = _build_emulation_system() + template = get_thermal_dispatch_template_network(CopperPlatePowerModel) + model = EmulationModel( + template, + sys; + optimizer = HiGHS_optimizer, + resolution = Hour(1), + initialize_model = false, + ) + @test build!(model; executions = 1, output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT + @test IOM.get_status(model) == IOM.ModelBuildStatus.BUILT +end + +@testset "EmulationModel handle_initial_conditions skip" begin + sys = _build_emulation_system() + template = get_thermal_dispatch_template_network(CopperPlatePowerModel) + model = EmulationModel( + template, + sys; + optimizer = HiGHS_optimizer, + resolution = Hour(1), + initialize_model = false, + ) + @test build!(model; executions = 1, output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT +end + +@testset "EmulationModel reset" begin + sys = _build_emulation_system() + template = get_thermal_dispatch_template_network(CopperPlatePowerModel) + model = EmulationModel( + template, + sys; + optimizer = HiGHS_optimizer, + resolution = Hour(1), + initialize_model = false, + ) + @test build!(model; executions = 1, output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT + # Build again to trigger reset path + @test build!(model; executions = 1, output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT +end + +@testset "EmulationModel progress meter disabled in tests" begin + @test !POM._progress_meter_enabled() +end diff --git a/test/test_network_constructors.jl b/test/test_network_constructors.jl index c5341c3..4497d41 100644 --- a/test/test_network_constructors.jl +++ b/test/test_network_constructors.jl @@ -72,3 +72,56 @@ end @test "4-5-i_1" in axes(cons)[1] end end + +############################################### +##### ADDITIONAL NETWORK CONSTRUCTOR TESTS #### +############################################### + +@testset "CopperPlatePowerModel basic build and solve" begin + system = PSB.build_system(PSITestSystems, "c_sys5") + template = get_thermal_dispatch_template_network(CopperPlatePowerModel) + model = DecisionModel(template, system; optimizer = HiGHS_optimizer) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT + @test solve!(model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED +end + +@testset "PTDFPowerModel basic build and solve" begin + system = PSB.build_system(PSITestSystems, "c_sys5") + template = get_thermal_dispatch_template_network( + NetworkModel(PTDFPowerModel; PTDF_matrix = PTDF(system)), + ) + set_device_model!(template, DeviceModel(Line, StaticBranch)) + model = DecisionModel(template, system; optimizer = HiGHS_optimizer) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT + @test solve!(model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED +end + +@testset "PTDFPowerModel with network slacks" begin + system = PSB.build_system(PSITestSystems, "c_sys5") + template = get_thermal_dispatch_template_network( + NetworkModel(PTDFPowerModel; PTDF_matrix = PTDF(system), use_slacks = true), + ) + set_device_model!(template, DeviceModel(Line, StaticBranch)) + model = DecisionModel(template, system; optimizer = HiGHS_optimizer) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT + @test solve!(model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED +end + +@testset "PTDFPowerModel with duals" begin + system = PSB.build_system(PSITestSystems, "c_sys5") + template = get_thermal_dispatch_template_network( + NetworkModel( + PTDFPowerModel; + PTDF_matrix = PTDF(system), + duals = [CopperPlateBalanceConstraint], + ), + ) + set_device_model!(template, DeviceModel(Line, StaticBranch)) + model = DecisionModel(template, system; optimizer = HiGHS_optimizer) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT + @test solve!(model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED +end diff --git a/test/test_services_extended.jl b/test/test_services_extended.jl new file mode 100644 index 0000000..5d0df9b --- /dev/null +++ b/test/test_services_extended.jl @@ -0,0 +1,261 @@ +################################### +#### RAMP RESERVE TESTS ############ +################################### + +@testset "RampReserve with ThermalStandard" begin + c_sys5 = PSB.build_system(PSITestSystems, "c_sys5_uc"; add_reserves = true) + template = OperationsProblemTemplate(CopperPlatePowerModel) + set_device_model!(template, ThermalStandard, ThermalBasicDispatch) + set_device_model!(template, PowerLoad, StaticPowerLoad) + set_service_model!( + template, + ServiceModel(VariableReserve{ReserveUp}, RampReserve, "Reserve1"), + ) + set_service_model!( + template, + ServiceModel(VariableReserve{ReserveDown}, RampReserve, "Reserve2"), + ) + model = DecisionModel(template, c_sys5; optimizer = HiGHS_optimizer) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT +end + +@testset "RampReserve with ramp-limited generators" begin + c_sys5 = PSB.build_system(PSITestSystems, "c_ramp_test") + template = OperationsProblemTemplate(CopperPlatePowerModel) + set_device_model!(template, ThermalStandard, ThermalStandardDispatch) + set_device_model!(template, PowerLoad, StaticPowerLoad) + set_service_model!( + template, + ServiceModel(VariableReserve{ReserveUp}, RampReserve, "test_reserve"), + ) + model = DecisionModel(template, c_sys5; optimizer = HiGHS_optimizer) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT + @test solve!(model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED +end + +################################### +#### RESERVE WITH SLACKS ########## +################################### + +@testset "Reserve with Slacks enabled" begin + c_sys5 = PSB.build_system(PSITestSystems, "c_sys5_uc"; add_reserves = true) + template = OperationsProblemTemplate(CopperPlatePowerModel) + set_device_model!(template, ThermalStandard, ThermalStandardDispatch) + set_device_model!(template, PowerLoad, StaticPowerLoad) + set_service_model!( + template, + ServiceModel( + VariableReserve{ReserveUp}, + RangeReserve, + "Reserve1"; + use_slacks = true, + ), + ) + model = DecisionModel(template, c_sys5; optimizer = HiGHS_optimizer) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT +end + +################################### +#### NONSPINNING RESERVE TESTS ############ +########################################### + +@testset "NonSpinningReserve constraint verification" begin + c_sys5 = PSB.build_system(PSITestSystems, "c_sys5_uc_non_spin"; add_reserves = true) + template = get_thermal_standard_uc_template() + set_device_model!( + template, + DeviceModel(ThermalMultiStart, ThermalStandardUnitCommitment), + ) + set_service_model!( + template, + ServiceModel( + VariableReserveNonSpinning, + NonSpinningReserve, + "NonSpinningReserve", + ), + ) + model = DecisionModel(template, c_sys5; optimizer = HiGHS_optimizer) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT + + container = IOM.get_optimization_container(model) + # Verify reserve power constraint exists + @test IOM.ConstraintKey( + POM.ReservePowerConstraint, + PSY.VariableReserveNonSpinning, + "NonSpinningReserve", + ) in keys(IOM.get_constraints(container)) + + @test solve!(model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED +end + +########################################### +##### GROUP RESERVE TESTS ################# +########################################### + +@testset "GroupReserve with ConstantReserveGroup" begin + c_sys5 = PSB.build_system(PSITestSystems, "c_sys5_uc"; add_reserves = true) + reserve1 = get_component(VariableReserve{ReserveUp}, c_sys5, "Reserve1") + reserve11 = get_component(VariableReserve{ReserveUp}, c_sys5, "Reserve11") + group = ConstantReserveGroup{ReserveUp}(; + name = "group_reserve", + available = true, + requirement = 50.0, + ext = Dict{String, Any}(), + contributing_services = Service[reserve1, reserve11], + ) + add_component!(c_sys5, group) + for gen in PSY.get_contributing_devices(c_sys5, reserve1) + add_service!(gen, group, c_sys5) + end + + template = OperationsProblemTemplate(CopperPlatePowerModel) + set_device_model!(template, ThermalStandard, ThermalBasicDispatch) + set_device_model!(template, PowerLoad, StaticPowerLoad) + set_service_model!( + template, + ServiceModel(VariableReserve{ReserveUp}, RangeReserve, "Reserve1"), + ) + set_service_model!( + template, + ServiceModel(VariableReserve{ReserveUp}, RangeReserve, "Reserve11"), + ) + set_service_model!( + template, + ServiceModel(ConstantReserveGroup{ReserveUp}, GroupReserve, "group_reserve"), + ) + + model = DecisionModel(template, c_sys5; optimizer = HiGHS_optimizer) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT +end + +@testset "ConstantMaxInterfaceFlow with PTDFPowerModel" begin + c_sys5 = PSB.build_system(PSISystems, "two_area_pjm_DA") + transform_single_time_series!(c_sys5, Hour(24), Hour(1)) + template = get_thermal_dispatch_template_network( + NetworkModel(PTDFPowerModel; PTDF_matrix = PTDF(c_sys5)), + ) + set_device_model!(template, Line, StaticBranch) + set_device_model!(template, AreaInterchange, StaticBranch) + + for iface in get_components(TransmissionInterface, c_sys5) + set_service_model!( + template, + ServiceModel( + TransmissionInterface, + ConstantMaxInterfaceFlow, + get_name(iface), + ), + ) + end + + model = DecisionModel( + template, + c_sys5; + resolution = Hour(1), + optimizer = HiGHS_optimizer, + ) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT + @test solve!(model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED +end + +########################################### +##### TRANSMISSION INTERFACE WITH SLACKS ## +########################################### + +@testset "ConstantMaxInterfaceFlow with slacks" begin + c_sys5 = PSB.build_system(PSISystems, "two_area_pjm_DA") + transform_single_time_series!(c_sys5, Hour(24), Hour(1)) + template = get_thermal_dispatch_template_network( + NetworkModel(PTDFPowerModel; PTDF_matrix = PTDF(c_sys5)), + ) + set_device_model!(template, Line, StaticBranch) + set_device_model!(template, AreaInterchange, StaticBranch) + + for iface in get_components(TransmissionInterface, c_sys5) + set_service_model!( + template, + ServiceModel( + TransmissionInterface, + ConstantMaxInterfaceFlow, + get_name(iface); + use_slacks = true, + ), + ) + end + + model = DecisionModel( + template, + c_sys5; + resolution = Hour(1), + optimizer = HiGHS_optimizer, + ) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT + + @test solve!(model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED +end + +########################################### +##### CONSTANT RESERVE WITH RANGE RESERVE # +########################################### + +@testset "ConstantReserve with RangeReserve formulation" begin + c_sys5 = PSB.build_system(PSITestSystems, "c_sys5_uc"; add_reserves = true) + template = OperationsProblemTemplate(CopperPlatePowerModel) + set_device_model!(template, ThermalStandard, ThermalBasicDispatch) + set_device_model!(template, PowerLoad, StaticPowerLoad) + set_service_model!( + template, + ServiceModel(VariableReserve{ReserveUp}, RangeReserve, "Reserve1"), + ) + set_service_model!( + template, + ServiceModel(VariableReserve{ReserveDown}, RangeReserve, "Reserve2"), + ) + model = DecisionModel(template, c_sys5; optimizer = HiGHS_optimizer) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT + + container = IOM.get_optimization_container(model) + # Verify requirement constraint exists + @test IOM.ConstraintKey( + POM.RequirementConstraint, + PSY.VariableReserve{PSY.ReserveUp}, + "Reserve1", + ) in keys(IOM.get_constraints(container)) + @test solve!(model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED +end + +########################################### +##### RESERVE WITH SOLVE VERIFICATION ##### +########################################### + +@testset "RampReserve with solve and constraint verification" begin + c_sys5 = PSB.build_system(PSITestSystems, "c_sys5_uc"; add_reserves = true) + template = OperationsProblemTemplate(CopperPlatePowerModel) + set_device_model!(template, ThermalStandard, ThermalBasicDispatch) + set_device_model!(template, PowerLoad, StaticPowerLoad) + set_service_model!( + template, + ServiceModel(VariableReserve{ReserveUp}, RampReserve, "Reserve1"), + ) + model = DecisionModel(template, c_sys5; optimizer = HiGHS_optimizer) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT + + container = IOM.get_optimization_container(model) + # Verify ramp constraint was added + @test IOM.ConstraintKey( + POM.RampConstraint, + PSY.VariableReserve{PSY.ReserveUp}, + "Reserve1", + ) in keys(IOM.get_constraints(container)) + + @test solve!(model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED +end diff --git a/test/test_storage_device_models.jl b/test/test_storage_device_models.jl index cbc92a9..6cc4e6a 100644 --- a/test/test_storage_device_models.jl +++ b/test/test_storage_device_models.jl @@ -362,3 +362,47 @@ end moi_tests(model, 121, 0, 74, 72, 24, true) end =# + +@testset "Storage with Cycling Limits" begin + device_model = DeviceModel( + EnergyReservoirStorage, + StorageDispatchWithReserves; + attributes = Dict{String, Any}( + "reservation" => false, + "cycling_limits" => true, + "energy_target" => false, + "complete_coverage" => false, + "regularization" => true, + ), + ) + c_sys5_bat = PSB.build_system(PSITestSystems, "c_sys5_bat_ems") + template = get_thermal_dispatch_template_network(CopperPlatePowerModel) + set_device_model!(template, device_model) + + model = DecisionModel(template, c_sys5_bat; optimizer = HiGHS_optimizer) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + ModelBuildStatus.BUILT + @test solve!(model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED +end + +@testset "Storage with Energy Target" begin + device_model = DeviceModel( + EnergyReservoirStorage, + StorageDispatchWithReserves; + attributes = Dict{String, Any}( + "reservation" => false, + "cycling_limits" => false, + "energy_target" => true, + "complete_coverage" => false, + "regularization" => true, + ), + ) + c_sys5_bat = PSB.build_system(PSITestSystems, "c_sys5_bat_ems") + template = get_thermal_dispatch_template_network(CopperPlatePowerModel) + set_device_model!(template, device_model) + + model = DecisionModel(template, c_sys5_bat; optimizer = HiGHS_optimizer) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + ModelBuildStatus.BUILT + @test solve!(model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED +end diff --git a/test/test_transfer_initial_conditions.jl b/test/test_transfer_initial_conditions.jl index 34f8877..98df86c 100644 --- a/test/test_transfer_initial_conditions.jl +++ b/test/test_transfer_initial_conditions.jl @@ -33,3 +33,83 @@ # ED should still solve with the transferred ICs @test solve!(ed) == IOM.RunStatus.SUCCESSFULLY_FINALIZED end + +@testset "transfer_initial_conditions! with EnergyReservoirStorage" begin + sys_uc = PSB.build_system(PSITestSystems, "c_sys5_bat_ems") + sys_ed = PSB.build_system(PSITestSystems, "c_sys5_bat_ems") + + template_uc = get_template_standard_uc_simulation() + set_device_model!( + template_uc, + DeviceModel( + EnergyReservoirStorage, + StorageDispatchWithReserves; + attributes = Dict{String, Any}( + "reservation" => false, + "cycling_limits" => false, + "energy_target" => false, + "complete_coverage" => false, + "regularization" => true, + ), + ), + ) + template_ed = get_template_nomin_ed_simulation() + set_device_model!( + template_ed, + DeviceModel( + EnergyReservoirStorage, + StorageDispatchWithReserves; + attributes = Dict{String, Any}( + "reservation" => false, + "cycling_limits" => false, + "energy_target" => false, + "complete_coverage" => false, + "regularization" => true, + ), + ), + ) + + uc = DecisionModel(template_uc, sys_uc; name = "UC", optimizer = HiGHS_optimizer) + ed = DecisionModel(template_ed, sys_ed; name = "ED", optimizer = HiGHS_optimizer) + + @test build!(uc; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT + @test solve!(uc) == IOM.RunStatus.SUCCESSFULLY_FINALIZED + + @test build!(ed; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT + + POM.transfer_initial_conditions!(ed, uc) + @test solve!(ed) == IOM.RunStatus.SUCCESSFULLY_FINALIZED +end + +@testset "transfer_initial_conditions! with HydroReservoir" begin + sys_uc = PSB.build_system(PSITestSystems, "c_sys5_hyd") + sys_ed = PSB.build_system(PSITestSystems, "c_sys5_hyd") + + template_uc = get_template_standard_uc_simulation() + set_device_model!( + template_uc, + HydroDispatch, + HydroDispatchRunOfRiver, + ) + template_ed = get_template_nomin_ed_simulation() + set_device_model!( + template_ed, + HydroDispatch, + HydroDispatchRunOfRiver, + ) + + uc = DecisionModel(template_uc, sys_uc; name = "UC", optimizer = HiGHS_optimizer) + ed = DecisionModel(template_ed, sys_ed; name = "ED", optimizer = HiGHS_optimizer) + + @test build!(uc; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT + @test solve!(uc) == IOM.RunStatus.SUCCESSFULLY_FINALIZED + + @test build!(ed; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT + + POM.transfer_initial_conditions!(ed, uc) + @test solve!(ed) == IOM.RunStatus.SUCCESSFULLY_FINALIZED +end diff --git a/test/test_utils.jl b/test/test_utils.jl index dfa38cd..a7e0a3c 100644 --- a/test/test_utils.jl +++ b/test/test_utils.jl @@ -44,3 +44,13 @@ end # Note: "Test simulation output directory name" from PSI omitted because # _get_output_dir_name is in PSI's simulation code and hasn't been split out. + +@testset "generate_formulation_combinations" begin + combos = POM.generate_formulation_combinations() + @test combos isa Dict + @test !isempty(combos) + @test haskey(combos, "device_formulations") + @test haskey(combos, "service_formulations") + @test !isempty(combos["device_formulations"]) + @test !isempty(combos["service_formulations"]) +end