diff --git a/scripts/coverage/Project.toml b/scripts/coverage/Project.toml new file mode 100644 index 00000000..a7b2880f --- /dev/null +++ b/scripts/coverage/Project.toml @@ -0,0 +1,7 @@ +[deps] +Coverage = "a2441757-f6aa-5fb2-8edb-039e3f45d037" +CoverageTools = "c36e975a-824b-4404-a568-ef97ca766997" + +[compat] +Coverage = "1" +CoverageTools = "1" diff --git a/scripts/generate_lcov.jl b/scripts/generate_lcov.jl index d16dd23a..0092a990 100644 --- a/scripts/generate_lcov.jl +++ b/scripts/generate_lcov.jl @@ -5,7 +5,7 @@ # # 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")' +# 2. Generate lcov: julia --project=scripts/coverage -e 'using Pkg; Pkg.instantiate(); include("scripts/generate_lcov.jl")' using CoverageTools using Coverage diff --git a/scripts/test_with_coverage.sh b/scripts/test_with_coverage.sh index f32ac543..251ccee2 100755 --- a/scripts/test_with_coverage.sh +++ b/scripts/test_with_coverage.sh @@ -14,9 +14,7 @@ julia --project=. --code-coverage -e ' ' echo "==> Generating lcov.info..." -julia --project=. -e ' - using TestEnv; TestEnv.activate() - include("scripts/generate_lcov.jl") -' +# Coverage.jl caps HTTP at 1.x while the OpenAPI packages need 2.x, so it has its own env. +julia --project=scripts/coverage -e 'using Pkg; Pkg.instantiate(); include("scripts/generate_lcov.jl")' echo "==> Done. lcov.info written to $(pwd)/lcov.info" diff --git a/scripts/units_dispatch_profile.jl b/scripts/units_dispatch_profile.jl index 3ef6340f..0aa20730 100644 --- a/scripts/units_dispatch_profile.jl +++ b/scripts/units_dispatch_profile.jl @@ -75,13 +75,13 @@ function make_container(devices) end # Build N components, evenly distributed across the 3 unit systems. The cost -# *curves* are concretely typed (CostCurve{LinearCurve, NaturalUnit/SU/DU}) but +# *curves* are concretely typed (CostCurve{LinearCurve, NaturalUnit/SU/CU}) but # the abstractly-typed `MockThermalGen` storage means upstream callers see a # UnionAll. This is the realistic shape of consuming code. function make_workload() devices = MockThermalGen[] curves = Any[] # heterogeneous on U, simulates abstract field upstream - units = (IS.NaturalUnit(), IS.SystemBaseUnit(), IS.DeviceBaseUnit()) + units = (IS.NaturalUnit(), IS.SystemBaseUnit(), IS.ComponentBaseUnit()) for i in 1:N_COMPONENTS u = units[mod1(i, 3)] push!(devices, make_mock_thermal("g$i"; base_power = 50.0 + i)) diff --git a/src/operation/problem_template.jl b/src/operation/problem_template.jl index 2c1a001a..4f80d639 100644 --- a/src/operation/problem_template.jl +++ b/src/operation/problem_template.jl @@ -165,11 +165,29 @@ function finalize_template!(template::AbstractProblemTemplate, args...) ) end +""" + share_template_references!(template_::AbstractProblemTemplate, template::AbstractProblemTemplate) + +Extension point for template types whose build-time copy must share some objects with +the caller's `template` by reference instead of by `deepcopy` — for instance objects a +build mutates that callers then inspect on the instance they passed in. The model +constructors call it on the fresh copy `template_`; the default shares nothing. +""" +function share_template_references!(::AbstractProblemTemplate, ::AbstractProblemTemplate) + return +end + +function _deepcopy_template(template::AbstractProblemTemplate) + template_ = _deepcopy_sharing_network_data(template) + share_template_references!(template_, template) + return template_ +end + # Deep-copy a template while sharing the network source and derived data by reference: # both can hold PNM matrices whose solver caches carry raw factorization handles and # deliberately error on deepcopy (PNM #312). Sharing is safe because instantiation # replaces the copy's `network_data` outright rather than mutating the original's. -function _deepcopy_template(template::AbstractProblemTemplate) +function _deepcopy_sharing_network_data(template::AbstractProblemTemplate) network_model = get_network_model(template) network_model === nothing && return deepcopy(template) source = network_model.network_source diff --git a/src/utils/component_utils.jl b/src/utils/component_utils.jl index f45e118c..8b537acc 100644 --- a/src/utils/component_utils.jl +++ b/src/utils/component_utils.jl @@ -73,6 +73,22 @@ end ########### Cost Function Utilities ############## ################################################## +""" + _system_base_ratio(unit_system, system_base_power, device_base_power) -> Float64 + +The x-axis ratio `x_from = ratio * x_su` from `unit_system` to the system base, the +base arithmetic `IS.convert_cost_coefficient` no longer resolves itself. +""" +_system_base_ratio(::IS.SystemBaseUnit, ::Float64, ::Float64) = 1.0 +_system_base_ratio( + ::IS.ComponentBaseUnit, + system_base_power::Float64, + device_base_power::Float64, +) = + system_base_power / device_base_power +_system_base_ratio(::IS.NaturalUnit, system_base_power::Float64, ::Float64) = + system_base_power + """ Proportional (slope) cost coefficient normalized to system base. """ @@ -82,8 +98,8 @@ get_proportional_cost_per_system_unit( system_base_power::Float64, device_base_power::Float64, ) = IS.convert_cost_coefficient( - cost_term, unit_system, IS.SU, - system_base_power, device_base_power, + cost_term, + _system_base_ratio(unit_system, system_base_power, device_base_power), ) """ @@ -95,8 +111,9 @@ get_quadratic_cost_per_system_unit( system_base_power::Float64, device_base_power::Float64, ) = IS.convert_cost_coefficient( - cost_term, unit_system, IS.SU, - system_base_power, device_base_power, 2, + cost_term, + _system_base_ratio(unit_system, system_base_power, device_base_power), + 2, ) """ @@ -110,8 +127,9 @@ function get_piecewise_pointcurve_per_system_unit( device_base_power::Float64, ) x_ratio = IS.convert_cost_coefficient( - 1.0, unit_system, IS.SU, - system_base_power, device_base_power, -1, + 1.0, + _system_base_ratio(unit_system, system_base_power, device_base_power), + -1, ) points = cost_component.points points_normalized = similar(points) @@ -165,14 +183,9 @@ function get_piecewise_curve_per_system_unit( system_base_power::Float64, device_base_power::Float64, ) - x_ratio = IS.convert_cost_coefficient( - 1.0, unit_system, IS.SU, - system_base_power, device_base_power, -1, - ) - y_ratio = IS.convert_cost_coefficient( - 1.0, unit_system, IS.SU, - system_base_power, device_base_power, 1, - ) + ratio = _system_base_ratio(unit_system, system_base_power, device_base_power) + x_ratio = IS.convert_cost_coefficient(1.0, ratio, -1) + y_ratio = IS.convert_cost_coefficient(1.0, ratio, 1) return x_coords .* x_ratio, y_coords .* y_ratio end diff --git a/test/InfrastructureOptimizationModelsTests.jl b/test/InfrastructureOptimizationModelsTests.jl index 82afd6ae..37ec8306 100644 --- a/test/InfrastructureOptimizationModelsTests.jl +++ b/test/InfrastructureOptimizationModelsTests.jl @@ -116,12 +116,14 @@ function run_tests() # TODO outputs_by_time.jl # TODO service_model.jl include(joinpath(TEST_DIR, "test_settings.jl")) + include(joinpath(TEST_DIR, "test_problem_template.jl")) # standard_variables_expressions.jl: low complexity # time_series_parameter_types.jl: low complexity # --- objective_function/ subfolder --- # import_export.jl: commented out include(joinpath(TEST_DIR, "test_cost_term_helpers.jl")) + include(joinpath(TEST_DIR, "test_cost_unit_conversion.jl")) include(joinpath(TEST_DIR, "test_linear_curve.jl")) # value_curve_cost.jl: needs more work include(joinpath(TEST_DIR, "test_piecewise_linear.jl")) diff --git a/test/Project.toml b/test/Project.toml index 6ed66685..d4312690 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -2,8 +2,6 @@ Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" ArgParse = "c7e460c6-2fb9-53a9-8c5b-16f535851c63" 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_cost_unit_conversion.jl b/test/test_cost_unit_conversion.jl new file mode 100644 index 00000000..3f3eb957 --- /dev/null +++ b/test/test_cost_unit_conversion.jl @@ -0,0 +1,97 @@ +""" +Unit tests for the cost-coefficient base conversions in src/utils/component_utils.jl. +`IS.convert_cost_coefficient` takes a bare x-axis ratio; the helpers derive it from the +curve's unit system and the two base powers. +""" + +@testset "Cost coefficient conversion to system base" begin + system_base = 100.0 + device_base = 20.0 + + @testset "proportional term" begin + @test IOM.get_proportional_cost_per_system_unit( + 3.0, + IS.SU, + system_base, + device_base, + ) == 3.0 + # \$/MW -> \$/(system pu MW): one system pu is 100 MW + @test IOM.get_proportional_cost_per_system_unit( + 3.0, + IS.NU, + system_base, + device_base, + ) == 300.0 + # \$/(device pu MW) -> \$/(system pu MW): one system pu is five device pu + @test IOM.get_proportional_cost_per_system_unit( + 3.0, + IS.CU, + system_base, + device_base, + ) == 15.0 + end + + @testset "quadratic term scales with the square of the ratio" begin + @test IOM.get_quadratic_cost_per_system_unit( + 2.0, + IS.SU, + system_base, + device_base, + ) == 2.0 + @test IOM.get_quadratic_cost_per_system_unit( + 2.0, + IS.NU, + system_base, + device_base, + ) == 2.0e4 + @test IOM.get_quadratic_cost_per_system_unit( + 2.0, + IS.CU, + system_base, + device_base, + ) == 50.0 + end + + @testset "point curve rescales x only" begin + curve = IS.PiecewiseLinearData([(x = 0.0, y = 0.0), (x = 20.0, y = 100.0)]) + natural = IOM.get_piecewise_pointcurve_per_system_unit( + curve, + IS.NU, + system_base, + device_base, + ) + @test IS.get_x_coords(natural) == [0.0, 0.2] + @test IS.get_y_coords(natural) == [0.0, 100.0] + device = IOM.get_piecewise_pointcurve_per_system_unit( + curve, + IS.CU, + system_base, + device_base, + ) + @test IS.get_x_coords(device) == [0.0, 4.0] + @test IOM.get_piecewise_pointcurve_per_system_unit( + curve, + IS.SU, + system_base, + device_base, + ) === curve + end + + @testset "step curve rescales x down and y up" begin + curve = IS.PiecewiseStepData([0.0, 20.0], [5.0]) + natural = + IOM.get_piecewise_curve_per_system_unit(curve, IS.NU, system_base, device_base) + @test IS.get_x_coords(natural) == [0.0, 0.2] + @test IS.get_y_coords(natural) == [500.0] + device = + IOM.get_piecewise_curve_per_system_unit(curve, IS.CU, system_base, device_base) + @test IS.get_x_coords(device) == [0.0, 4.0] + @test IS.get_y_coords(device) == [25.0] + @test IOM.get_piecewise_curve_per_system_unit( + curve, + IS.SU, + system_base, + device_base, + ) === curve + end +end diff --git a/test/test_linear_curve.jl b/test/test_linear_curve.jl index 0443fd7d..8398b30a 100644 --- a/test/test_linear_curve.jl +++ b/test/test_linear_curve.jl @@ -232,7 +232,7 @@ end # Cost: 30 $/p.u.h in device base units cost_curve = IS.CostCurve( IS.LinearCurve(30.0), - IS.DeviceBaseUnit(), + IS.ComponentBaseUnit(), ) InfrastructureOptimizationModels.add_variable_cost_to_objective!( @@ -312,7 +312,7 @@ end ), DEVICE_BASE = IS.CostCurve( IS.LinearCurve(rate * device_base), - IS.DeviceBaseUnit(), + IS.ComponentBaseUnit(), ), ) diff --git a/test/test_problem_template.jl b/test/test_problem_template.jl new file mode 100644 index 00000000..b0b98ff2 --- /dev/null +++ b/test/test_problem_template.jl @@ -0,0 +1,43 @@ +""" +Unit tests for the template deep-copy path and its `share_template_references!` hook. +""" + +using Test +using InfrastructureOptimizationModels + +# Minimal templates: one shares a field through the hook, one relies on the default. +mutable struct SharingTestTemplate <: IOM.AbstractProblemTemplate + shared::Vector{Int} + cloned::Vector{Int} +end +IOM.get_network_model(::SharingTestTemplate) = nothing +function IOM.share_template_references!( + template_::SharingTestTemplate, + template::SharingTestTemplate, +) + template_.shared = template.shared + return +end + +mutable struct DefaultCopyTestTemplate <: IOM.AbstractProblemTemplate + data::Vector{Int} +end +IOM.get_network_model(::DefaultCopyTestTemplate) = nothing + +@testset "Template deep copy" begin + @testset "share_template_references! runs on the copy" begin + template = SharingTestTemplate([1], [2]) + template_ = IOM._deepcopy_template(template) + @test template_ !== template + @test template_.shared === template.shared + @test template_.cloned == template.cloned + @test template_.cloned !== template.cloned + end + + @testset "default hook shares nothing" begin + template = DefaultCopyTestTemplate([1]) + template_ = IOM._deepcopy_template(template) + @test template_.data == template.data + @test template_.data !== template.data + end +end diff --git a/test/test_quadratic_curve.jl b/test/test_quadratic_curve.jl index 48aefb81..cf0b33e1 100644 --- a/test/test_quadratic_curve.jl +++ b/test/test_quadratic_curve.jl @@ -318,7 +318,7 @@ end cost_curve = IS.CostCurve( IS.QuadraticCurve(1.0, 20.0, 0.0), - IS.DeviceBaseUnit(), + IS.ComponentBaseUnit(), ) InfrastructureOptimizationModels.add_variable_cost_to_objective!( @@ -404,7 +404,7 @@ end ), DEVICE_BASE = IS.CostCurve( IS.QuadraticCurve(a * device_base^2, b * device_base, 0.0), - IS.DeviceBaseUnit(), + IS.ComponentBaseUnit(), ), ) diff --git a/test/test_ts_value_curve_objective.jl b/test/test_ts_value_curve_objective.jl index f6ddef48..a75015e6 100644 --- a/test/test_ts_value_curve_objective.jl +++ b/test/test_ts_value_curve_objective.jl @@ -351,7 +351,7 @@ end for (unit_system, expected_slope_factor, expected_bp_factor) in [ (IS.NaturalUnit(), system_base, 1.0 / system_base), ( - IS.DeviceBaseUnit(), + IS.ComponentBaseUnit(), 1.0 / (device_base / system_base), device_base / system_base, ),