From 55dcc1d4bb9dd6368311e1dc9be3170ca8cfe0d5 Mon Sep 17 00:00:00 2001 From: Luke Kiernan Date: Fri, 21 Nov 2025 14:12:35 -0700 Subject: [PATCH 1/2] InputOutputCurve function evaluation --- src/function_data.jl | 19 ++++++++++++++++--- src/value_curve.jl | 9 +++++++-- test/test_cost_functions.jl | 29 +++++++++++++++++++++++++++++ test/test_function_data.jl | 20 ++++++++++++++++++++ 4 files changed, 72 insertions(+), 5 deletions(-) diff --git a/src/function_data.jl b/src/function_data.jl index 172c163c2..60784ae50 100644 --- a/src/function_data.jl +++ b/src/function_data.jl @@ -649,11 +649,24 @@ _eval_fd_impl( ) = y_coords[i] -"Evaluate the `PiecewiseLinearData` or `PiecewiseStepData` at a given x-coordinate" -function (fd::Union{PiecewiseLinearData, PiecewiseStepData})(x::Real) +function check_domain(fd::FunctionData, x::Real) + lb, ub = get_domain(fd) + (lb <= x <= ub) || + throw(ArgumentError("x=$x is outside the domain [$lb, $ub]")) + return x +end + +"Helper function to check domain with floating point tolerance." +function check_domain(fd::FunctionData, x::AbstractFloat) lb, ub = get_domain(fd) - (x < lb || x > ub) && + ((lb <= x <= ub) || isapprox(x, lb) || isapprox(x, ub)) || throw(ArgumentError("x=$x is outside the domain [$lb, $ub]")) + return clamp(x, lb, ub) +end + +"Evaluate the `PiecewiseLinearData` or `PiecewiseStepData` at a given x-coordinate" +function (fd::Union{PiecewiseLinearData, PiecewiseStepData})(x::Real) + x = check_domain(fd, x) x_coords = get_x_coords(fd) y_coords = get_y_coords(fd) i_leq = searchsortedlast(x_coords, x) # uses binary search! diff --git a/src/value_curve.jl b/src/value_curve.jl index 669cb6050..c9c221464 100644 --- a/src/value_curve.jl +++ b/src/value_curve.jl @@ -41,6 +41,11 @@ InputOutputCurve{T}( ) where {(T <: Union{QuadraticFunctionData, LinearFunctionData, PiecewiseLinearData})} = InputOutputCurve{T}(function_data, nothing) +""" +Evaluate the `InputOutputCurve` at a given input value `x`. +""" +(ioc::InputOutputCurve)(x::Real) = get_function_data(ioc)(x) + """ An incremental (or 'marginal') curve, relating the production quantity to the derivative of cost: `y = f'(x)`. Can be used, for instance, in the representation of a [`CostCurve`](@ref) @@ -62,7 +67,7 @@ IncrementalCurve(function_data, initial_input) = IncrementalCurve{T}( function_data, initial_input, -) where {(T <: Union{QuadraticFunctionData, LinearFunctionData, PiecewiseLinearData})} = +) where {(T <: Union{LinearFunctionData, PiecewiseStepData})} = IncrementalCurve{T}(function_data, initial_input, nothing) """ @@ -87,7 +92,7 @@ AverageRateCurve(function_data, initial_input) = AverageRateCurve{T}( function_data, initial_input, -) where {(T <: Union{QuadraticFunctionData, LinearFunctionData, PiecewiseLinearData})} = +) where {(T <: Union{LinearFunctionData, PiecewiseStepData})} = AverageRateCurve{T}(function_data, initial_input, nothing) "Get the `initial_input` field of this `ValueCurve` (not defined for `InputOutputCurve`)" diff --git a/test/test_cost_functions.jl b/test/test_cost_functions.jl index 412337549..333a6a877 100644 --- a/test/test_cost_functions.jl +++ b/test/test_cost_functions.jl @@ -337,3 +337,32 @@ end ) == IS.LinearCurve(10.0, 7.0) end + +@testset "Test prohibited FunctionData types" begin + # Incremental and Average Rate curves only support + # linear and piecewise step function data. + q_fd = IS.QuadraticFunctionData(1, 2, 3) + pwl_fd = IS.PiecewiseLinearData([(x = 0.0, y = 1.0), (x = 1.0, y = 2.0)]) + @test_throws MethodError IS.IncrementalCurve(q_fd, 0.0) + @test_throws MethodError IS.AverageRateCurve(q_fd, 0.0) + @test_throws MethodError IS.IncrementalCurve(pwl_fd, 0.0) + @test_throws MethodError IS.AverageRateCurve(pwl_fd, 0.0) +end + +@testset "Test InputOutputCurve evaluation" begin + io_quadratic = IS.InputOutputCurve(IS.QuadraticFunctionData(1, 2, 3)) + @test io_quadratic(0.0) == 3.0 + @test io_quadratic(1.0) == 6.0 + @test io_quadratic(2.0) == 11.0 + + io_linear = IS.InputOutputCurve(IS.LinearFunctionData(3, 2)) + @test io_linear(0.0) == 2.0 + @test io_linear(1.0) == 5.0 + @test io_linear(2.0) == 8.0 + + pwl = IS.PiecewiseLinearData([(x = 1, y = 3), (x = 3, y = 7), (x = 5, y = 11)]) + io_piecewise = IS.InputOutputCurve(pwl) + @test io_piecewise(1.0) == 3.0 + @test io_piecewise(3.0) == 7.0 + @test io_piecewise(5.0) == 11.0 +end diff --git a/test/test_function_data.jl b/test/test_function_data.jl index 33b8b5a5c..fdb866553 100644 --- a/test/test_function_data.jl +++ b/test/test_function_data.jl @@ -689,3 +689,23 @@ end compact_plain_ans end end + +@testset "Test piecewise domain checking" begin + pwl = IS.PiecewiseStepData([1, 3, 5], [8, 10]) + + # floating point inputs + @test_throws ArgumentError pwl(0.5) + @test_throws ArgumentError pwl(5.5) + pwl(2.5) + + # non floating point inputs + @test_throws ArgumentError pwl(1 // 2) + @test_throws ArgumentError pwl(5 + 1 // 2) + pwl(5 // 2) + + # floating point precision edge cases (should not error) + @assert isapprox(1 - eps() / 2, 1) + @assert isapprox(5 + eps() / 2, 5) + pwl(1 - eps() / 2) + pwl(5 + eps() / 2) +end From 69859fd0666611233bc8dc5547e87ae83418b42b Mon Sep 17 00:00:00 2001 From: Luke Kiernan Date: Fri, 21 Nov 2025 14:48:07 -0700 Subject: [PATCH 2/2] put domain check inside function --- src/function_data.jl | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/src/function_data.jl b/src/function_data.jl index 60784ae50..94eecfd28 100644 --- a/src/function_data.jl +++ b/src/function_data.jl @@ -649,24 +649,13 @@ _eval_fd_impl( ) = y_coords[i] -function check_domain(fd::FunctionData, x::Real) - lb, ub = get_domain(fd) - (lb <= x <= ub) || - throw(ArgumentError("x=$x is outside the domain [$lb, $ub]")) - return x -end - -"Helper function to check domain with floating point tolerance." -function check_domain(fd::FunctionData, x::AbstractFloat) +"Evaluate the `PiecewiseLinearData` or `PiecewiseStepData` at a given x-coordinate" +function (fd::Union{PiecewiseLinearData, PiecewiseStepData})(x::Real) lb, ub = get_domain(fd) + # defend against floating point precision issues at the boundaries. ((lb <= x <= ub) || isapprox(x, lb) || isapprox(x, ub)) || throw(ArgumentError("x=$x is outside the domain [$lb, $ub]")) - return clamp(x, lb, ub) -end - -"Evaluate the `PiecewiseLinearData` or `PiecewiseStepData` at a given x-coordinate" -function (fd::Union{PiecewiseLinearData, PiecewiseStepData})(x::Real) - x = check_domain(fd, x) + x = clamp(x, lb, ub) x_coords = get_x_coords(fd) y_coords = get_y_coords(fd) i_leq = searchsortedlast(x_coords, x) # uses binary search!