diff --git a/src/InfrastructureSystems.jl b/src/InfrastructureSystems.jl index 6a54f1ee4..99cb28199 100644 --- a/src/InfrastructureSystems.jl +++ b/src/InfrastructureSystems.jl @@ -2,6 +2,10 @@ isdefined(Base, :__precompile__) && __precompile__() module InfrastructureSystems +# Cost aliases don't display properly unless they are exported from IS +export LinearCurve, QuadraticCurve +export PiecewisePointCurve, PiecewiseIncrementalCurve, PiecewiseAverageCurve + import Base: @kwdef import CSV import DataFrames @@ -140,6 +144,9 @@ include("validation.jl") include("utils/print.jl") include("utils/test.jl") include("units.jl") +include("value_curve.jl") +include("cost_aliases.jl") +include("production_variable_cost_curve.jl") include("deprecated.jl") include("Optimization/Optimization.jl") include("Simulation/Simulation.jl") diff --git a/src/cost_aliases.jl b/src/cost_aliases.jl new file mode 100644 index 000000000..011450fba --- /dev/null +++ b/src/cost_aliases.jl @@ -0,0 +1,199 @@ +# Cost aliases: a simplified interface to the portion of the parametric +# `ValueCurve{FunctionData}` design that the user is likely to interact with. Each alias +# consists of a simple name for a particular `ValueCurve{FunctionData}` type, a constructor +# and methods to interact with it without having to think about `FunctionData`, and +# overridden printing behavior to complete the illusion. Everything here (aside from the +# overridden printing) is properly speaking mere syntactic sugar for the underlying +# `ValueCurve{FunctionData}` design. One could imagine similar convenience constructors and +# methods being defined for all the `ValueCurve{FunctionData}` types, not just the ones we +# have here nicely packaged and presented to the user. + +"Whether there is a cost alias for the instance or type under consideration" +is_cost_alias(::Union{ValueCurve, Type{<:ValueCurve}}) = false + +""" + LinearCurve(proportional_term::Float64) + LinearCurve(proportional_term::Float64, constant_term::Float64) + +A linear input-output curve, representing a constant marginal rate. May have zero no-load +cost (i.e., constant average rate) or not. + +# Arguments +- `proportional_term::Float64`: marginal rate +- `constant_term::Float64`: optional, cost at zero production, defaults to `0.0` +""" +const LinearCurve = InputOutputCurve{LinearFunctionData} + +is_cost_alias(::Union{LinearCurve, Type{LinearCurve}}) = true + +InputOutputCurve{LinearFunctionData}(proportional_term::Real) = + InputOutputCurve(LinearFunctionData(proportional_term)) + +InputOutputCurve{LinearFunctionData}(proportional_term::Real, constant_term::Real) = + InputOutputCurve(LinearFunctionData(proportional_term, constant_term)) + +"Get the proportional term (i.e., slope) of the `LinearCurve`" +get_proportional_term(vc::LinearCurve) = get_proportional_term(get_function_data(vc)) + +"Get the constant term (i.e., intercept) of the `LinearCurve`" +get_constant_term(vc::LinearCurve) = get_constant_term(get_function_data(vc)) + +Base.show(io::IO, vc::LinearCurve) = + if isnothing(get_input_at_zero(vc)) + print(io, "$(typeof(vc))($(get_proportional_term(vc)), $(get_constant_term(vc)))") + else + Base.show_default(io, vc) + end + +""" + QuadraticCurve(quadratic_term::Float64, proportional_term::Float64, constant_term::Float64) + +A quadratic input-output curve, may have nonzero no-load cost. + +# Arguments +- `quadratic_term::Float64`: quadratic term of the curve +- `proportional_term::Float64`: proportional term of the curve +- `constant_term::Float64`: constant term of the curve +""" +const QuadraticCurve = InputOutputCurve{QuadraticFunctionData} + +is_cost_alias(::Union{QuadraticCurve, Type{QuadraticCurve}}) = true + +InputOutputCurve{QuadraticFunctionData}(quadratic_term, proportional_term, constant_term) = + InputOutputCurve( + QuadraticFunctionData(quadratic_term, proportional_term, constant_term), + ) + +"Get the quadratic term of the `QuadraticCurve`" +get_quadratic_term(vc::QuadraticCurve) = get_quadratic_term(get_function_data(vc)) + +"Get the proportional (i.e., linear) term of the `QuadraticCurve`" +get_proportional_term(vc::QuadraticCurve) = get_proportional_term(get_function_data(vc)) + +"Get the constant term of the `QuadraticCurve`" +get_constant_term(vc::QuadraticCurve) = get_constant_term(get_function_data(vc)) + +Base.show(io::IO, vc::QuadraticCurve) = + if isnothing(get_input_at_zero(vc)) + print( + io, + "$(typeof(vc))($(get_quadratic_term(vc)), $(get_proportional_term(vc)), $(get_constant_term(vc)))", + ) + else + Base.show_default(io, vc) + end + +""" + PiecewisePointCurve(points::Vector{Tuple{Float64, Float64}}) + +A piecewise linear curve specified by cost values at production points. + +# Arguments +- `points::Vector{Tuple{Float64, Float64}}` or similar: vector of `(production, cost)` pairs +""" +const PiecewisePointCurve = InputOutputCurve{PiecewiseLinearData} + +is_cost_alias(::Union{PiecewisePointCurve, Type{PiecewisePointCurve}}) = true + +InputOutputCurve{PiecewiseLinearData}(points::Vector) = + InputOutputCurve(PiecewiseLinearData(points)) + +"Get the points that define the `PiecewisePointCurve`" +get_points(vc::PiecewisePointCurve) = get_points(get_function_data(vc)) + +"Get the x-coordinates of the points that define the `PiecewisePointCurve`" +get_x_coords(vc::PiecewisePointCurve) = get_x_coords(get_function_data(vc)) + +"Get the y-coordinates of the points that define the `PiecewisePointCurve`" +get_y_coords(vc::PiecewisePointCurve) = get_y_coords(get_function_data(vc)) + +"Calculate the slopes of the line segments defined by the `PiecewisePointCurve`" +get_slopes(vc::PiecewisePointCurve) = get_slopes(get_function_data(vc)) + +# Here we manually circumvent the @NamedTuple{x::Float64, y::Float64} type annotation, but we keep things looking like named tuples +Base.show(io::IO, vc::PiecewisePointCurve) = + if isnothing(get_input_at_zero(vc)) + print(io, "$(typeof(vc))([$(join(get_points(vc), ", "))])") + else + Base.show_default(io, vc) + end + +""" + PiecewiseIncrementalCurve(initial_input::Union{Float64, Nothing}, x_coords::Vector{Float64}, slopes::Vector{Float64}) + PiecewiseIncrementalCurve(input_at_zero::Union{Nothing, Float64}, initial_input::Union{Float64, Nothing}, x_coords::Vector{Float64}, slopes::Vector{Float64}) + +A piecewise linear curve specified by marginal rates (slopes) between production points. May +have nonzero initial value. + +# Arguments +- `input_at_zero::Union{Nothing, Float64}`: (optional, defaults to `nothing`) cost at zero production, does NOT represent a part of the curve +- `initial_input::Union{Float64, Nothing}`: cost at minimum production point `first(x_coords)` (NOT at zero production), defines the start of the curve +- `x_coords::Vector{Float64}`: vector of `n` production points +- `slopes::Vector{Float64}`: vector of `n-1` marginal rates/slopes of the curve segments between + the points +""" +const PiecewiseIncrementalCurve = IncrementalCurve{PiecewiseStepData} + +is_cost_alias(::Union{PiecewiseIncrementalCurve, Type{PiecewiseIncrementalCurve}}) = true + +IncrementalCurve{PiecewiseStepData}(initial_input, x_coords::Vector, slopes::Vector) = + IncrementalCurve(PiecewiseStepData(x_coords, slopes), initial_input) + +IncrementalCurve{PiecewiseStepData}( + input_at_zero, + initial_input, + x_coords::Vector, + slopes::Vector, +) = + IncrementalCurve(PiecewiseStepData(x_coords, slopes), initial_input, input_at_zero) + +"Get the x-coordinates that define the `PiecewiseIncrementalCurve`" +get_x_coords(vc::PiecewiseIncrementalCurve) = get_x_coords(get_function_data(vc)) + +"Fetch the slopes that define the `PiecewiseIncrementalCurve`" +get_slopes(vc::PiecewiseIncrementalCurve) = get_y_coords(get_function_data(vc)) + +Base.show(io::IO, vc::PiecewiseIncrementalCurve) = + print( + io, + if isnothing(get_input_at_zero(vc)) + "$(typeof(vc))($(get_initial_input(vc)), $(get_x_coords(vc)), $(get_slopes(vc)))" + else + "$(typeof(vc))($(get_input_at_zero(vc)), $(get_initial_input(vc)), $(get_x_coords(vc)), $(get_slopes(vc)))" + end, + ) + +""" + PiecewiseAverageCurve(initial_input::Union{Float64, Nothing}, x_coords::Vector{Float64}, slopes::Vector{Float64}) + +A piecewise linear curve specified by average rates between production points. May have +nonzero initial value. + +# Arguments +- `initial_input::Union{Float64, Nothing}`: cost at minimum production point `first(x_coords)` (NOT at zero production), defines the start of the curve +- `x_coords::Vector{Float64}`: vector of `n` production points +- `slopes::Vector{Float64}`: vector of `n-1` average rates/slopes of the curve segments between + the points +""" +const PiecewiseAverageCurve = AverageRateCurve{PiecewiseStepData} + +is_cost_alias(::Union{PiecewiseAverageCurve, Type{PiecewiseAverageCurve}}) = true + +AverageRateCurve{PiecewiseStepData}(initial_input, x_coords::Vector, y_coords::Vector) = + AverageRateCurve(PiecewiseStepData(x_coords, y_coords), initial_input) + +"Get the x-coordinates that define the `PiecewiseAverageCurve`" +get_x_coords(vc::PiecewiseAverageCurve) = get_x_coords(get_function_data(vc)) + +"Get the average rates that define the `PiecewiseAverageCurve`" +get_average_rates(vc::PiecewiseAverageCurve) = get_y_coords(get_function_data(vc)) + +Base.show(io::IO, vc::PiecewiseAverageCurve) = + if isnothing(get_input_at_zero(vc)) + print( + io, + "$(typeof(vc))($(get_initial_input(vc)), $(get_x_coords(vc)), $(get_average_rates(vc)))", + ) + else + Base.show_default(io, vc) + end diff --git a/src/production_variable_cost_curve.jl b/src/production_variable_cost_curve.jl new file mode 100644 index 000000000..b64ed486f --- /dev/null +++ b/src/production_variable_cost_curve.jl @@ -0,0 +1,149 @@ +abstract type ProductionVariableCostCurve{T <: ValueCurve} end + +serialize(val::ProductionVariableCostCurve) = serialize_struct(val) +deserialize(T::Type{<:ProductionVariableCostCurve}, val::Dict) = + deserialize_struct(T, val) + +"Get the underlying `ValueCurve` representation of this `ProductionVariableCostCurve`" +get_value_curve(cost::ProductionVariableCostCurve) = cost.value_curve +"Get the variable operation and maintenance cost in currency/(power_units h)" +get_vom_cost(cost::ProductionVariableCostCurve) = cost.vom_cost +"Get the units for the x-axis of the curve" +get_power_units(cost::ProductionVariableCostCurve) = cost.power_units +"Get the `FunctionData` representation of this `ProductionVariableCostCurve`'s `ValueCurve`" +get_function_data(cost::ProductionVariableCostCurve) = + get_function_data(get_value_curve(cost)) +"Get the `initial_input` field of this `ProductionVariableCostCurve`'s `ValueCurve` (not defined for input-output data)" +get_initial_input(cost::ProductionVariableCostCurve) = + get_initial_input(get_value_curve(cost)) +"Calculate the convexity of the underlying data" +is_convex(cost::ProductionVariableCostCurve) = is_convex(get_value_curve(cost)) + +Base.:(==)(a::T, b::T) where {T <: ProductionVariableCostCurve} = + double_equals_from_fields(a, b) + +Base.isequal(a::T, b::T) where {T <: ProductionVariableCostCurve} = + isequal_from_fields(a, b) + +Base.hash(a::ProductionVariableCostCurve) = hash_from_fields(a) + +""" +$(TYPEDEF) +$(TYPEDFIELDS) + + CostCurve(value_curve, power_units, vom_cost) + CostCurve(; value_curve, power_units, vom_cost) + +Direct representation of the variable operation cost of a power plant in currency. Composed +of a [`ValueCurve`](@ref) that may represent input-output, incremental, or average rate +data. The default units for the x-axis are MW and can be specified with +`power_units`. +""" +@kwdef struct CostCurve{T <: ValueCurve} <: ProductionVariableCostCurve{T} + "The underlying `ValueCurve` representation of this `ProductionVariableCostCurve`" + value_curve::T + "(default: natural units (MW)) The units for the x-axis of the curve" + power_units::UnitSystem = UnitSystem.NATURAL_UNITS + "(default of 0) Additional proportional Variable Operation and Maintenance Cost in + \$/(power_unit h), represented as a [`LinearCurve`](@ref)" + vom_cost::LinearCurve = LinearCurve(0.0) +end + +CostCurve(value_curve) = CostCurve(; value_curve) +CostCurve(value_curve, vom_cost::LinearCurve) = + CostCurve(; value_curve, vom_cost = vom_cost) +CostCurve(value_curve, power_units::UnitSystem) = + CostCurve(; value_curve, power_units = power_units) + +Base.:(==)(a::CostCurve, b::CostCurve) = + (get_value_curve(a) == get_value_curve(b)) && + (get_power_units(a) == get_power_units(b)) && + (get_vom_cost(a) == get_vom_cost(b)) + +"Get a `CostCurve` representing zero variable cost" +Base.zero(::Union{CostCurve, Type{CostCurve}}) = CostCurve(zero(ValueCurve)) + +""" +$(TYPEDEF) +$(TYPEDFIELDS) + + FuelCurve(value_curve, power_units, fuel_cost, vom_cost) + FuelCurve(value_curve, fuel_cost) + FuelCurve(value_curve, fuel_cost, vom_cost) + FuelCurve(value_curve, power_units, fuel_cost) + FuelCurve(; value_curve, power_units, fuel_cost, vom_cost) + +Representation of the variable operation cost of a power plant in terms of fuel (MBTU, +liters, m^3, etc.), coupled with a conversion factor between fuel and currency. Composed of +a [`ValueCurve`](@ref) that may represent input-output, incremental, or average rate data. +The default units for the x-axis are MW and can be specified with `power_units`. +""" +@kwdef struct FuelCurve{T <: ValueCurve} <: ProductionVariableCostCurve{T} + "The underlying `ValueCurve` representation of this `ProductionVariableCostCurve`" + value_curve::T + "(default: natural units (MW)) The units for the x-axis of the curve" + power_units::UnitSystem = UnitSystem.NATURAL_UNITS + "Either a fixed value for fuel cost or the key to a fuel cost time series" + fuel_cost::Union{Float64, TimeSeriesKey} + "(default of 0) Additional proportional Variable Operation and Maintenance Cost in \$/(power_unit h) + represented as a [`LinearCurve`](@ref)" + vom_cost::LinearCurve = LinearCurve(0.0) +end + +FuelCurve( + value_curve::ValueCurve, + power_units::UnitSystem, + fuel_cost::Real, + vom_cost::LinearCurve, +) = + FuelCurve(value_curve, power_units, Float64(fuel_cost), vom_cost) + +FuelCurve(value_curve, fuel_cost) = FuelCurve(; value_curve, fuel_cost) +FuelCurve(value_curve, fuel_cost::Union{Float64, TimeSeriesKey}, vom_cost::LinearCurve) = + FuelCurve(; value_curve, fuel_cost, vom_cost = vom_cost) +FuelCurve(value_curve, power_units::UnitSystem, fuel_cost::Union{Float64, TimeSeriesKey}) = + FuelCurve(; value_curve, power_units = power_units, fuel_cost = fuel_cost) + +Base.:(==)(a::FuelCurve, b::FuelCurve) = + (get_value_curve(a) == get_value_curve(b)) && + (get_power_units(a) == get_power_units(b)) && + (get_fuel_cost(a) == get_fuel_cost(b)) && + (get_vom_cost(a) == get_vom_cost(b)) + +"Get a `FuelCurve` representing zero fuel usage and zero fuel cost" +Base.zero(::Union{FuelCurve, Type{FuelCurve}}) = FuelCurve(zero(ValueCurve), 0.0) + +"Get the fuel cost or the name of the fuel cost time series" +get_fuel_cost(cost::FuelCurve) = cost.fuel_cost + +Base.show(io::IO, m::MIME"text/plain", curve::ProductionVariableCostCurve) = + (get(io, :compact, false)::Bool ? _show_compact : _show_expanded)(io, m, curve) + +# The strategy here is to put all the short stuff on the first line, then break and let the value_curve take more space +function _show_compact(io::IO, ::MIME"text/plain", curve::CostCurve) + print( + io, + "$(nameof(typeof(curve))) with power_units $(curve.power_units), vom_cost $(curve.vom_cost), and value_curve:\n ", + ) + vc_printout = sprint(show, "text/plain", curve.value_curve; context = io) # Capture the value_curve `show` so we can indent it + print(io, replace(vc_printout, "\n" => "\n ")) +end + +function _show_compact(io::IO, ::MIME"text/plain", curve::FuelCurve) + print( + io, + "$(nameof(typeof(curve))) with power_units $(curve.power_units), fuel_cost $(curve.fuel_cost), vom_cost $(curve.vom_cost), and value_curve:\n ", + ) + vc_printout = sprint(show, "text/plain", curve.value_curve; context = io) + print(io, replace(vc_printout, "\n" => "\n ")) +end + +function _show_expanded(io::IO, ::MIME"text/plain", curve::ProductionVariableCostCurve) + print(io, "$(nameof(typeof(curve))):") + for field_name in fieldnames(typeof(curve)) + val = getproperty(curve, field_name) + val_printout = + replace(sprint(show, "text/plain", val; context = io), "\n" => "\n ") + print(io, "\n $(field_name): $val_printout") + end +end diff --git a/src/value_curve.jl b/src/value_curve.jl new file mode 100644 index 000000000..1ffcf3e5e --- /dev/null +++ b/src/value_curve.jl @@ -0,0 +1,287 @@ +""" +Supertype that represents a unitless cost curve + +Concrete options are [listed here.](@ref value_curve_library) +""" +abstract type ValueCurve{T <: FunctionData} end + +# JSON SERIALIZATION +serialize(val::ValueCurve) = serialize_struct(val) +deserialize(T::Type{<:ValueCurve}, val::Dict) = deserialize_struct(T, val) + +"Get the underlying `FunctionData` representation of this `ValueCurve`" +get_function_data(curve::ValueCurve) = curve.function_data + +"Get the `input_at_zero` field of this `ValueCurve`" +get_input_at_zero(curve::ValueCurve) = curve.input_at_zero + +""" +An input-output curve, directly relating the production quantity to the cost: `y = f(x)`. +Can be used, for instance, in the representation of a [`CostCurve`](@ref) where `x` is MW +and `y` is currency/hr, or in the representation of a [`FuelCurve`](@ref) where `x` is MW +and `y` is fuel/hr. +""" +@kwdef struct InputOutputCurve{ + T <: Union{QuadraticFunctionData, LinearFunctionData, PiecewiseLinearData}, +} <: ValueCurve{T} + "The underlying `FunctionData` representation of this `ValueCurve`" + function_data::T + "Optional, an explicit representation of the input value at zero output." + input_at_zero::Union{Nothing, Float64} = nothing +end + +InputOutputCurve(function_data) = InputOutputCurve(function_data, nothing) +InputOutputCurve{T}( + function_data, +) where {(T <: Union{QuadraticFunctionData, LinearFunctionData, PiecewiseLinearData})} = + InputOutputCurve{T}(function_data, nothing) + +""" +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) +where `x` is MW and `y` is currency/MWh, or in the representation of a [`FuelCurve`](@ref) +where `x` is MW and `y` is fuel/MWh. +""" +@kwdef struct IncrementalCurve{T <: Union{LinearFunctionData, PiecewiseStepData}} <: + ValueCurve{T} + "The underlying `FunctionData` representation of this `ValueCurve`" + function_data::T + "The value of f(x) at the least x for which the function is defined, or the origin for functions with no left endpoint, used for conversion to `InputOutputCurve`" + initial_input::Union{Float64, Nothing} + "Optional, an explicit representation of the input value at zero output." + input_at_zero::Union{Nothing, Float64} = nothing +end + +IncrementalCurve(function_data, initial_input) = + IncrementalCurve(function_data, initial_input, nothing) +IncrementalCurve{T}( + function_data, + initial_input, +) where {(T <: Union{QuadraticFunctionData, LinearFunctionData, PiecewiseLinearData})} = + IncrementalCurve{T}(function_data, initial_input, nothing) + +""" +An average rate curve, relating the production quantity to the average cost rate from the +origin: `y = f(x)/x`. Can be used, for instance, in the representation of a +[`CostCurve`](@ref) where `x` is MW and `y` is currency/MWh, or in the representation of a +[`FuelCurve`](@ref) where `x` is MW and `y` is fuel/MWh. Typically calculated by dividing +absolute values of cost rate or fuel input rate by absolute values of electric power. +""" +@kwdef struct AverageRateCurve{T <: Union{LinearFunctionData, PiecewiseStepData}} <: + ValueCurve{T} + "The underlying `FunctionData` representation of this `ValueCurve`, in the case of `AverageRateCurve{LinearFunctionData}` representing only the oblique asymptote" + function_data::T + "The value of f(x) at the least x for which the function is defined, or the origin for functions with no left endpoint, used for conversion to `InputOutputCurve`" + initial_input::Union{Float64, Nothing} + "Optional, an explicit representation of the input value at zero output." + input_at_zero::Union{Nothing, Float64} = nothing +end + +AverageRateCurve(function_data, initial_input) = + AverageRateCurve(function_data, initial_input, nothing) +AverageRateCurve{T}( + function_data, + initial_input, +) where {(T <: Union{QuadraticFunctionData, LinearFunctionData, PiecewiseLinearData})} = + AverageRateCurve{T}(function_data, initial_input, nothing) + +"Get the `initial_input` field of this `ValueCurve` (not defined for `InputOutputCurve`)" +get_initial_input(curve::Union{IncrementalCurve, AverageRateCurve}) = curve.initial_input + +# BASE METHODS +Base.:(==)(a::T, b::T) where {T <: ValueCurve} = double_equals_from_fields(a, b) + +Base.isequal(a::T, b::T) where {T <: ValueCurve} = isequal_from_fields(a, b) + +Base.hash(a::ValueCurve) = hash_from_fields(a) + +"Get an `InputOutputCurve` representing `f(x) = 0`" +Base.zero(::Union{InputOutputCurve, Type{InputOutputCurve}}) = + InputOutputCurve(zero(FunctionData)) + +"Get an `IncrementalCurve` representing `f'(x) = 0` with zero `initial_input`" +Base.zero(::Union{IncrementalCurve, Type{IncrementalCurve}}) = + IncrementalCurve(zero(FunctionData), 0.0) + +"Get an `AverageRateCurve` representing `f(x)/x = 0` with zero `initial_input`" +Base.zero(::Union{AverageRateCurve, Type{AverageRateCurve}}) = + AverageRateCurve(zero(FunctionData), 0.0) + +"Get a `ValueCurve` representing zero variable cost" +Base.zero(::Union{ValueCurve, Type{ValueCurve}}) = + Base.zero(InputOutputCurve) + +# CONVERSIONS: InputOutputCurve{LinearFunctionData} to InputOutputCurve{QuadraticFunctionData} +InputOutputCurve{QuadraticFunctionData}(data::InputOutputCurve{LinearFunctionData}) = + InputOutputCurve{QuadraticFunctionData}( + get_function_data(data), + get_input_at_zero(data), + ) + +Base.convert( + ::Type{InputOutputCurve{QuadraticFunctionData}}, + data::InputOutputCurve{LinearFunctionData}, +) = InputOutputCurve{QuadraticFunctionData}(data) + +# CONVERSIONS: InputOutputCurve to X +function IncrementalCurve(data::InputOutputCurve{QuadraticFunctionData}) + fd = get_function_data(data) + q, p, c = get_quadratic_term(fd), get_proportional_term(fd), get_constant_term(fd) + return IncrementalCurve(LinearFunctionData(2q, p), c, get_input_at_zero(data)) +end + +function AverageRateCurve(data::InputOutputCurve{QuadraticFunctionData}) + fd = get_function_data(data) + q, p, c = get_quadratic_term(fd), get_proportional_term(fd), get_constant_term(fd) + return AverageRateCurve(LinearFunctionData(q, p), c, get_input_at_zero(data)) +end + +IncrementalCurve(data::InputOutputCurve{LinearFunctionData}) = + IncrementalCurve(InputOutputCurve{QuadraticFunctionData}(data)) + +AverageRateCurve(data::InputOutputCurve{LinearFunctionData}) = + AverageRateCurve(InputOutputCurve{QuadraticFunctionData}(data)) + +function IncrementalCurve(data::InputOutputCurve{PiecewiseLinearData}) + fd = get_function_data(data) + return IncrementalCurve( + PiecewiseStepData(get_x_coords(fd), get_slopes(fd)), + first(get_points(fd)).y, get_input_at_zero(data), + ) +end + +function AverageRateCurve(data::InputOutputCurve{PiecewiseLinearData}) + fd = get_function_data(data) + points = get_points(fd) + slopes_from_origin = [p.y / p.x for p in points[2:end]] + return AverageRateCurve( + PiecewiseStepData(get_x_coords(fd), slopes_from_origin), + first(points).y, get_input_at_zero(data), + ) +end + +# CONVERSIONS: IncrementalCurve to X +function InputOutputCurve(data::IncrementalCurve{LinearFunctionData}) + fd = get_function_data(data) + p = get_proportional_term(fd) + c = get_initial_input(data) + isnothing(c) && throw( + ArgumentError("Cannot convert `IncrementalCurve` with undefined `initial_input`"), + ) + (p == 0) && return InputOutputCurve( + LinearFunctionData(get_constant_term(fd), c), + ) + return InputOutputCurve( + QuadraticFunctionData(p / 2, get_constant_term(fd), c), + get_input_at_zero(data), + ) +end + +function InputOutputCurve(data::IncrementalCurve{PiecewiseStepData}) + fd = get_function_data(data) + c = get_initial_input(data) + isnothing(c) && throw( + ArgumentError("Cannot convert `IncrementalCurve` with undefined `initial_input`"), + ) + points = running_sum(fd) + return InputOutputCurve( + PiecewiseLinearData([(p.x, p.y + c) for p in points]), + get_input_at_zero(data), + ) +end + +AverageRateCurve(data::IncrementalCurve) = AverageRateCurve(InputOutputCurve(data)) + +# CONVERSIONS: AverageRateCurve to X +function InputOutputCurve(data::AverageRateCurve{LinearFunctionData}) + fd = get_function_data(data) + p = get_proportional_term(fd) + c = get_initial_input(data) + isnothing(c) && throw( + ArgumentError("Cannot convert `AverageRateCurve` with undefined `initial_input`"), + ) + (p == 0) && return InputOutputCurve( + LinearFunctionData(get_constant_term(fd), c), + get_input_at_zero(data), + ) + return InputOutputCurve( + QuadraticFunctionData(p, get_constant_term(fd), c), + get_input_at_zero(data), + ) +end + +function InputOutputCurve(data::AverageRateCurve{PiecewiseStepData}) + fd = get_function_data(data) + c = get_initial_input(data) + isnothing(c) && throw( + ArgumentError("Cannot convert `AverageRateCurve` with undefined `initial_input`"), + ) + xs = get_x_coords(fd) + ys = xs[2:end] .* get_y_coords(fd) + return InputOutputCurve( + PiecewiseLinearData(collect(zip(xs, vcat(c, ys)))), + get_input_at_zero(data), + ) +end + +IncrementalCurve(data::AverageRateCurve) = IncrementalCurve(InputOutputCurve(data)) + +# CALCULATIONS +is_convex(curve::InputOutputCurve) = is_convex(get_function_data(curve)) +"Calculate the convexity of the underlying data" +is_convex(curve::ValueCurve) = is_convex(InputOutputCurve(curve)) + +# PRINTING +# For cost aliases, return the alias name; otherwise, return the type name without the parameter +simple_type_name(curve::ValueCurve) = + string(is_cost_alias(curve) ? typeof(curve) : nameof(typeof(curve))) + +function Base.show(io::IO, ::MIME"text/plain", curve::InputOutputCurve) + print(io, simple_type_name(curve)) + is_cost_alias(curve) && print(io, " (a type of $InputOutputCurve)") + print(io, " where ") + !isnothing(get_input_at_zero(curve)) && + print(io, "value at zero is $(get_input_at_zero(curve)), ") + print(io, "function is: ") + show(IOContext(io, :compact => true), "text/plain", get_function_data(curve)) +end + +function Base.show(io::IO, ::MIME"text/plain", curve::IncrementalCurve) + print(io, simple_type_name(curve)) + print(io, " where ") + !isnothing(get_input_at_zero(curve)) && + print(io, "value at zero is $(get_input_at_zero(curve)), ") + print(io, "initial value is $(get_initial_input(curve))") + print(io, ", derivative function f is: ") + show(IOContext(io, :compact => true), "text/plain", get_function_data(curve)) +end + +function Base.show(io::IO, ::MIME"text/plain", curve::AverageRateCurve) + print(io, simple_type_name(curve)) + print(io, " where ") + !isnothing(get_input_at_zero(curve)) && + print(io, "value at zero is $(get_input_at_zero(curve)), ") + print(io, "initial value is $(get_initial_input(curve))") + print(io, ", average rate function f is: ") + show(IOContext(io, :compact => true), "text/plain", get_function_data(curve)) +end + +# MORE GENERIC CONSTRUCTORS +# These manually do what https://github.com/JuliaLang/julia/issues/35053 (open at time of writing) proposes to automatically provide +InputOutputCurve( + function_data::T, + input_at_zero, +) where {T <: Union{LinearFunctionData, QuadraticFunctionData, PiecewiseLinearData}} = + InputOutputCurve{T}(function_data, input_at_zero) +IncrementalCurve( + function_data::T, + initial_input, + input_at_zero, +) where {T <: Union{LinearFunctionData, PiecewiseStepData}} = + IncrementalCurve{T}(function_data, initial_input, input_at_zero) +AverageRateCurve( + function_data::T, + initial_input, + input_at_zero, +) where {T <: Union{LinearFunctionData, PiecewiseStepData}} = + AverageRateCurve{T}(function_data, initial_input, input_at_zero) diff --git a/test/test_cost_functions.jl b/test/test_cost_functions.jl new file mode 100644 index 000000000..39b8ba7af --- /dev/null +++ b/test/test_cost_functions.jl @@ -0,0 +1,325 @@ +# Get all possible isomorphic representations of the given `ValueCurve` +function all_conversions(vc::IS.ValueCurve; + universe = (IS.InputOutputCurve, IS.IncrementalCurve, IS.AverageRateCurve), +) + convert_to = filter(!=(nameof(typeof(vc))) ∘ nameof, universe) # x -> nameof(x) != nameof(typeof(vc)) + result = Set{IS.ValueCurve}(constructor(vc) for constructor in convert_to) + (vc isa IS.InputOutputCurve{IS.LinearFunctionData}) && + push!(result, IS.InputOutputCurve{IS.QuadraticFunctionData}(vc)) + return result +end + +@testset "Test ValueCurves" begin + # IS.InputOutputCurve + io_quadratic = IS.InputOutputCurve(IS.QuadraticFunctionData(3, 2, 1)) + @test io_quadratic isa IS.InputOutputCurve{IS.QuadraticFunctionData} + @test IS.get_function_data(io_quadratic) == IS.QuadraticFunctionData(3, 2, 1) + @test IS.IncrementalCurve(io_quadratic) == + IS.IncrementalCurve(IS.LinearFunctionData(6, 2), 1.0) + @test IS.AverageRateCurve(io_quadratic) == + IS.AverageRateCurve(IS.LinearFunctionData(3, 2), 1.0) + @test zero(io_quadratic) == IS.InputOutputCurve(IS.LinearFunctionData(0, 0)) + @test zero(IS.InputOutputCurve) == IS.InputOutputCurve(IS.LinearFunctionData(0, 0)) + @test IS.is_cost_alias(io_quadratic) == IS.is_cost_alias(typeof(io_quadratic)) == true + @test repr(io_quadratic) == sprint(show, io_quadratic) == + "QuadraticCurve(3.0, 2.0, 1.0)" + @test sprint(show, "text/plain", io_quadratic) == + "QuadraticCurve (a type of InfrastructureSystems.InputOutputCurve) where function is: f(x) = 3.0 x^2 + 2.0 x + 1.0" + + io_linear = IS.InputOutputCurve(IS.LinearFunctionData(2, 1)) + @test io_linear isa IS.InputOutputCurve{IS.LinearFunctionData} + @test IS.get_function_data(io_linear) == IS.LinearFunctionData(2, 1) + @test IS.InputOutputCurve{IS.QuadraticFunctionData}(io_linear) == + IS.InputOutputCurve(IS.QuadraticFunctionData(0, 2, 1)) + @test IS.IncrementalCurve(io_linear) == + IS.IncrementalCurve(IS.LinearFunctionData(0, 2), 1.0) + @test IS.AverageRateCurve(io_linear) == + IS.AverageRateCurve(IS.LinearFunctionData(0, 2), 1.0) + @test IS.is_cost_alias(io_linear) == IS.is_cost_alias(typeof(io_linear)) == true + @test repr(io_linear) == sprint(show, io_linear) == + "LinearCurve(2.0, 1.0)" + @test sprint(show, "text/plain", io_linear) == + "LinearCurve (a type of InfrastructureSystems.InputOutputCurve) where function is: f(x) = 2.0 x + 1.0" + + io_piecewise = IS.InputOutputCurve(IS.PiecewiseLinearData([(1, 6), (3, 9), (5, 13)])) + @test io_piecewise isa IS.InputOutputCurve{IS.PiecewiseLinearData} + @test IS.get_function_data(io_piecewise) == + IS.PiecewiseLinearData([(1, 6), (3, 9), (5, 13)]) + @test IS.IncrementalCurve(io_piecewise) == + IS.IncrementalCurve(IS.PiecewiseStepData([1, 3, 5], [1.5, 2]), 6.0) + @test IS.AverageRateCurve(io_piecewise) == + IS.AverageRateCurve(IS.PiecewiseStepData([1, 3, 5], [3, 2.6]), 6.0) + @test IS.is_cost_alias(io_piecewise) == IS.is_cost_alias(typeof(io_piecewise)) == true + @test repr(io_piecewise) == sprint(show, io_piecewise) == + "PiecewisePointCurve([(x = 1.0, y = 6.0), (x = 3.0, y = 9.0), (x = 5.0, y = 13.0)])" + @test sprint(show, "text/plain", io_piecewise) == + "PiecewisePointCurve (a type of InfrastructureSystems.InputOutputCurve) where function is: piecewise linear y = f(x) connecting points:\n (x = 1.0, y = 6.0)\n (x = 3.0, y = 9.0)\n (x = 5.0, y = 13.0)" + + # IS.IncrementalCurve + inc_linear = IS.IncrementalCurve(IS.LinearFunctionData(6, 2), 1.0) + inc_linear_no_initial = IS.IncrementalCurve(IS.LinearFunctionData(6, 2), nothing) + @test inc_linear isa IS.IncrementalCurve{IS.LinearFunctionData} + @test inc_linear_no_initial isa IS.IncrementalCurve{IS.LinearFunctionData} + @test IS.get_function_data(inc_linear) == IS.LinearFunctionData(6, 2) + @test IS.get_initial_input(inc_linear) == 1 + @test IS.InputOutputCurve(inc_linear) == + IS.InputOutputCurve(IS.QuadraticFunctionData(3, 2, 1)) + @test IS.InputOutputCurve(IS.IncrementalCurve(IS.LinearFunctionData(0, 2), 1.0)) == + IS.InputOutputCurve(IS.LinearFunctionData(2, 1)) + @test IS.AverageRateCurve(inc_linear) == + IS.AverageRateCurve(IS.LinearFunctionData(3, 2), 1.0) + @test_throws ArgumentError IS.InputOutputCurve(inc_linear_no_initial) + @test_throws ArgumentError IS.AverageRateCurve(inc_linear_no_initial) + @test zero(inc_linear) == IS.IncrementalCurve(IS.LinearFunctionData(0, 0), 0.0) + @test zero(IS.IncrementalCurve) == IS.IncrementalCurve(IS.LinearFunctionData(0, 0), 0.0) + @test IS.is_cost_alias(inc_linear) == IS.is_cost_alias(typeof(inc_linear)) == false + @test repr(inc_linear) == sprint(show, inc_linear) == + "InfrastructureSystems.IncrementalCurve{InfrastructureSystems.LinearFunctionData}(InfrastructureSystems.LinearFunctionData(6.0, 2.0), 1.0, nothing)" + @test sprint(show, "text/plain", inc_linear) == + "IncrementalCurve where initial value is 1.0, derivative function f is: f(x) = 6.0 x + 2.0" + + inc_piecewise = IS.IncrementalCurve(IS.PiecewiseStepData([1, 3, 5], [1.5, 2]), 6.0) + inc_piecewise_no_initial = + IS.IncrementalCurve(IS.PiecewiseStepData([1, 3, 5], [1.5, 2]), nothing) + @test inc_piecewise isa IS.IncrementalCurve{IS.PiecewiseStepData} + @test inc_piecewise_no_initial isa IS.IncrementalCurve{IS.PiecewiseStepData} + @test IS.get_function_data(inc_piecewise) == IS.PiecewiseStepData([1, 3, 5], [1.5, 2]) + @test IS.get_initial_input(inc_piecewise) == 6 + @test IS.InputOutputCurve(inc_piecewise) == + IS.InputOutputCurve(IS.PiecewiseLinearData([(1, 6), (3, 9), (5, 13)])) + @test IS.AverageRateCurve(inc_piecewise) == + IS.AverageRateCurve(IS.PiecewiseStepData([1, 3, 5], [3, 2.6]), 6.0) + @test_throws ArgumentError IS.InputOutputCurve(inc_piecewise_no_initial) + @test_throws ArgumentError IS.AverageRateCurve(inc_piecewise_no_initial) + @test IS.is_cost_alias(inc_piecewise) == IS.is_cost_alias(typeof(inc_piecewise)) == + true + @test repr(inc_piecewise) == sprint(show, inc_piecewise) == + "PiecewiseIncrementalCurve(6.0, [1.0, 3.0, 5.0], [1.5, 2.0])" + @test sprint(show, "text/plain", inc_piecewise) == + "PiecewiseIncrementalCurve where initial value is 6.0, derivative function f is: f(x) =\n 1.5 for x in [1.0, 3.0)\n 2.0 for x in [3.0, 5.0)" + + # IS.AverageRateCurve + ar_linear = IS.AverageRateCurve(IS.LinearFunctionData(3, 2), 1.0) + ar_linear_no_initial = IS.AverageRateCurve(IS.LinearFunctionData(3, 2), nothing) + @test ar_linear isa IS.AverageRateCurve{IS.LinearFunctionData} + @test ar_linear_no_initial isa IS.AverageRateCurve{IS.LinearFunctionData} + @test IS.get_function_data(ar_linear) == IS.LinearFunctionData(3, 2) + @test IS.get_initial_input(ar_linear) == 1 + @test IS.InputOutputCurve(ar_linear) == + IS.InputOutputCurve(IS.QuadraticFunctionData(3, 2, 1)) + @test IS.InputOutputCurve(IS.AverageRateCurve(IS.LinearFunctionData(0, 2), 1.0)) == + IS.InputOutputCurve(IS.LinearFunctionData(2, 1)) + @test IS.IncrementalCurve(ar_linear) == + IS.IncrementalCurve(IS.LinearFunctionData(6, 2), 1.0) + @test_throws ArgumentError IS.InputOutputCurve(ar_linear_no_initial) + @test_throws ArgumentError IS.IncrementalCurve(ar_linear_no_initial) + @test zero(ar_linear) == IS.AverageRateCurve(IS.LinearFunctionData(0, 0), 0.0) + @test zero(IS.AverageRateCurve) == IS.AverageRateCurve(IS.LinearFunctionData(0, 0), 0.0) + @test IS.is_cost_alias(ar_linear) == IS.is_cost_alias(typeof(ar_linear)) == false + @test repr(ar_linear) == sprint(show, ar_linear) == + "InfrastructureSystems.AverageRateCurve{InfrastructureSystems.LinearFunctionData}(InfrastructureSystems.LinearFunctionData(3.0, 2.0), 1.0, nothing)" + @test sprint(show, "text/plain", ar_linear) == + "AverageRateCurve where initial value is 1.0, average rate function f is: f(x) = 3.0 x + 2.0" + + ar_piecewise = IS.AverageRateCurve(IS.PiecewiseStepData([1, 3, 5], [3, 2.6]), 6.0) + ar_piecewise_no_initial = + IS.AverageRateCurve(IS.PiecewiseStepData([1, 3, 5], [3, 2.6]), nothing) + @test ar_piecewise isa IS.AverageRateCurve{IS.PiecewiseStepData} + @test ar_piecewise_no_initial isa IS.AverageRateCurve{IS.PiecewiseStepData} + @test IS.get_function_data(ar_piecewise) == IS.PiecewiseStepData([1, 3, 5], [3, 2.6]) + @test IS.get_initial_input(ar_piecewise) == 6 + @test IS.InputOutputCurve(ar_piecewise) == + IS.InputOutputCurve(IS.PiecewiseLinearData([(1, 6), (3, 9), (5, 13)])) + @test IS.IncrementalCurve(ar_piecewise) == + IS.IncrementalCurve(IS.PiecewiseStepData([1, 3, 5], [1.5, 2]), 6.0) + @test_throws ArgumentError IS.InputOutputCurve(ar_piecewise_no_initial) + @test_throws ArgumentError IS.IncrementalCurve(ar_piecewise_no_initial) + @test IS.is_cost_alias(ar_piecewise) == IS.is_cost_alias(typeof(ar_piecewise)) == true + @test repr(ar_piecewise) == sprint(show, ar_piecewise) == + "PiecewiseAverageCurve(6.0, [1.0, 3.0, 5.0], [3.0, 2.6])" + @test sprint(show, "text/plain", ar_piecewise) == + "PiecewiseAverageCurve where initial value is 6.0, average rate function f is: f(x) =\n 3.0 for x in [1.0, 3.0)\n 2.6 for x in [3.0, 5.0)" + + # Serialization round trip + curves_by_type = [ # typeof() gives parameterized types + (io_quadratic, IS.InputOutputCurve), + (io_linear, IS.InputOutputCurve), + (io_piecewise, IS.InputOutputCurve), + (inc_linear, IS.IncrementalCurve), + (inc_piecewise, IS.IncrementalCurve), + (ar_linear, IS.AverageRateCurve), + (ar_piecewise, IS.AverageRateCurve), + (inc_linear_no_initial, IS.IncrementalCurve), + (inc_piecewise_no_initial, IS.IncrementalCurve), + (ar_linear_no_initial, IS.AverageRateCurve), + (ar_piecewise_no_initial, IS.AverageRateCurve), + ] + for (curve, curve_type) in curves_by_type + @test IS.serialize(curve) isa AbstractDict + @test IS.deserialize(curve_type, IS.serialize(curve)) == curve + end + + @test zero(IS.ValueCurve) == IS.InputOutputCurve(IS.LinearFunctionData(0, 0)) +end + +@testset "Test ValueCurve type conversion constructors" begin + @test IS.InputOutputCurve(IS.QuadraticFunctionData(3, 2, 1), 1) == + IS.InputOutputCurve(IS.QuadraticFunctionData(3, 2, 1), 1.0) + @test IS.IncrementalCurve(IS.LinearFunctionData(6, 2), 1) == + IS.IncrementalCurve(IS.LinearFunctionData(6, 2), 1.0) + @test IS.AverageRateCurve(IS.LinearFunctionData(3, 2), 1) == + IS.AverageRateCurve(IS.LinearFunctionData(3, 2), 1.0) +end + +@testset "Test cost aliases" begin + lc = IS.LinearCurve(3.0, 5.0) + @test lc == IS.InputOutputCurve(IS.LinearFunctionData(3.0, 5.0)) + @test IS.LinearCurve(3.0) == IS.InputOutputCurve(IS.LinearFunctionData(3.0, 0.0)) + @test IS.get_proportional_term(lc) == 3.0 + @test IS.get_constant_term(lc) == 5.0 + + qc = IS.QuadraticCurve(1.0, 2.0, 18.0) + @test qc == IS.InputOutputCurve(IS.QuadraticFunctionData(1.0, 2.0, 18.0)) + @test IS.get_quadratic_term(qc) == 1.0 + @test IS.get_proportional_term(qc) == 2.0 + @test IS.get_constant_term(qc) == 18.0 + + ppc = IS.PiecewisePointCurve([(1.0, 20.0), (2.0, 24.0), (3.0, 30.0)]) + @test ppc == + IS.InputOutputCurve( + IS.PiecewiseLinearData([(1.0, 20.0), (2.0, 24.0), (3.0, 30.0)]), + ) + @test IS.get_points(ppc) == + [(x = 1.0, y = 20.0), (x = 2.0, y = 24.0), (x = 3.0, y = 30.0)] + @test IS.get_x_coords(ppc) == [1.0, 2.0, 3.0] + @test IS.get_y_coords(ppc) == [20.0, 24.0, 30.0] + @test IS.get_slopes(ppc) == [4.0, 6.0] + + pic = IS.PiecewiseIncrementalCurve(20.0, [1.0, 2.0, 3.0], [4.0, 6.0]) + @test pic == + IS.IncrementalCurve(IS.PiecewiseStepData([1.0, 2.0, 3.0], [4.0, 6.0]), 20.0) + @test IS.get_x_coords(pic) == [1.0, 2.0, 3.0] + @test IS.get_slopes(pic) == [4.0, 6.0] + + pac = IS.PiecewiseAverageCurve(20.0, [1.0, 2.0, 3.0], [12.0, 10.0]) + @test pac == + IS.AverageRateCurve(IS.PiecewiseStepData([1.0, 2.0, 3.0], [12.0, 10.0]), 20.0) + @test IS.get_x_coords(pac) == [1.0, 2.0, 3.0] + @test IS.get_average_rates(pac) == [12.0, 10.0] + + # Make sure the aliases get registered properly + @test sprint(show, "text/plain", IS.QuadraticCurve) == + "QuadraticCurve (alias for InfrastructureSystems.InputOutputCurve{InfrastructureSystems.QuadraticFunctionData})" +end + +@testset "Test input_at_zero" begin + iaz = 1234.5 + pwinc_without_iaz = + IS.IncrementalCurve(IS.PiecewiseStepData([1, 3, 5], [1.5, 2]), 6.0, nothing) + pwinc_with_iaz = + IS.IncrementalCurve(IS.PiecewiseStepData([1, 3, 5], [1.5, 2]), 6.0, iaz) + all_without_iaz = [ + IS.InputOutputCurve(IS.QuadraticFunctionData(3, 2, 1), nothing), + IS.InputOutputCurve(IS.LinearFunctionData(2, 1), nothing), + IS.InputOutputCurve(IS.PiecewiseLinearData([(1, 6), (3, 9), (5, 13)]), nothing), + IS.IncrementalCurve(IS.LinearFunctionData(6, 2), 1.0, nothing), + pwinc_without_iaz, + IS.AverageRateCurve(IS.LinearFunctionData(3, 2), 1.0, nothing), + IS.AverageRateCurve(IS.PiecewiseStepData([1, 3, 5], [3, 2.6]), 6.0, nothing), + ] + all_with_iaz = [ + IS.InputOutputCurve(IS.QuadraticFunctionData(3, 2, 1), iaz), + IS.InputOutputCurve(IS.LinearFunctionData(2, 1), iaz), + IS.InputOutputCurve(IS.PiecewiseLinearData([(1, 6), (3, 9), (5, 13)]), iaz), + IS.IncrementalCurve(IS.LinearFunctionData(6, 2), 1.0, iaz), + pwinc_with_iaz, + IS.AverageRateCurve(IS.LinearFunctionData(3, 2), 1.0, iaz), + IS.AverageRateCurve(IS.PiecewiseStepData([1, 3, 5], [3, 2.6]), 6.0, iaz), + ] + + # Alias constructors + @test IS.PiecewiseIncrementalCurve(1234.5, 6.0, [1.0, 3.0, 5.0], [1.5, 2.0]) == + pwinc_with_iaz + + # Getters and printouts + for (without_iaz, with_iaz) in zip(all_without_iaz, all_with_iaz) + @test IS.get_input_at_zero(without_iaz) === nothing + @test IS.get_input_at_zero(with_iaz) == iaz + @test occursin(string(iaz), repr(with_iaz)) + @test sprint(show, with_iaz) == repr(with_iaz) + @test occursin(string(iaz), sprint(show, "text/plain", with_iaz)) + end + + @test repr(pwinc_with_iaz) == sprint(show, pwinc_with_iaz) == + "PiecewiseIncrementalCurve(1234.5, 6.0, [1.0, 3.0, 5.0], [1.5, 2.0])" + @test sprint(show, "text/plain", pwinc_with_iaz) == + "PiecewiseIncrementalCurve where value at zero is 1234.5, initial value is 6.0, derivative function f is: f(x) =\n 1.5 for x in [1.0, 3.0)\n 2.0 for x in [3.0, 5.0)" + + # Preserved under conversion + for without_iaz in Iterators.flatten(all_conversions.(all_without_iaz)) + @test IS.get_input_at_zero(without_iaz) === nothing + end + for with_iaz in Iterators.flatten(all_conversions.(all_with_iaz)) + @test IS.get_input_at_zero(with_iaz) == iaz + end +end + +@testset "Test IS.CostCurve and IS.FuelCurve" begin + cc = IS.CostCurve(IS.InputOutputCurve(IS.QuadraticFunctionData(1, 2, 3))) + fc = IS.FuelCurve(IS.InputOutputCurve(IS.QuadraticFunctionData(1, 2, 3)), 4.0) + # TODO also test fuel curves with time series + + @test IS.get_value_curve(cc) == IS.InputOutputCurve(IS.QuadraticFunctionData(1, 2, 3)) + @test IS.get_value_curve(fc) == IS.InputOutputCurve(IS.QuadraticFunctionData(1, 2, 3)) + @test IS.get_fuel_cost(fc) == 4 + + @test IS.serialize(cc) isa AbstractDict + @test IS.serialize(fc) isa AbstractDict + @test IS.deserialize(IS.CostCurve, IS.serialize(cc)) == cc + @test IS.deserialize(IS.FuelCurve, IS.serialize(fc)) == fc + + @test zero(cc) == IS.CostCurve(IS.InputOutputCurve(IS.LinearFunctionData(0.0, 0.0))) + @test zero(IS.CostCurve) == + IS.CostCurve(IS.InputOutputCurve(IS.LinearFunctionData(0.0, 0.0))) + @test zero(fc) == + IS.FuelCurve(IS.InputOutputCurve(IS.LinearFunctionData(0.0, 0.0)), 0.0) + @test zero(IS.FuelCurve) == + IS.FuelCurve(IS.InputOutputCurve(IS.LinearFunctionData(0.0, 0.0)), 0.0) + + @test repr(cc) == sprint(show, cc) == + "InfrastructureSystems.CostCurve{QuadraticCurve}(QuadraticCurve(1.0, 2.0, 3.0), InfrastructureSystems.UnitSystemModule.UnitSystem.NATURAL_UNITS = 2, LinearCurve(0.0, 0.0))" + @test repr(fc) == sprint(show, fc) == + "InfrastructureSystems.FuelCurve{QuadraticCurve}(QuadraticCurve(1.0, 2.0, 3.0), InfrastructureSystems.UnitSystemModule.UnitSystem.NATURAL_UNITS = 2, 4.0, LinearCurve(0.0, 0.0))" + @test sprint(show, "text/plain", cc) == + sprint(show, "text/plain", cc; context = :compact => false) == + "CostCurve:\n value_curve: QuadraticCurve (a type of InfrastructureSystems.InputOutputCurve) where function is: f(x) = 1.0 x^2 + 2.0 x + 3.0\n power_units: InfrastructureSystems.UnitSystemModule.UnitSystem.NATURAL_UNITS = 2\n vom_cost: LinearCurve (a type of InfrastructureSystems.InputOutputCurve) where function is: f(x) = 0.0 x + 0.0" + @test sprint(show, "text/plain", fc) == + sprint(show, "text/plain", fc; context = :compact => false) == + "FuelCurve:\n value_curve: QuadraticCurve (a type of InfrastructureSystems.InputOutputCurve) where function is: f(x) = 1.0 x^2 + 2.0 x + 3.0\n power_units: InfrastructureSystems.UnitSystemModule.UnitSystem.NATURAL_UNITS = 2\n fuel_cost: 4.0\n vom_cost: LinearCurve (a type of InfrastructureSystems.InputOutputCurve) where function is: f(x) = 0.0 x + 0.0" + @test sprint(show, "text/plain", cc; context = :compact => true) == + "CostCurve with power_units InfrastructureSystems.UnitSystemModule.UnitSystem.NATURAL_UNITS = 2, vom_cost LinearCurve(0.0, 0.0), and value_curve:\n QuadraticCurve (a type of InfrastructureSystems.InputOutputCurve) where function is: f(x) = 1.0 x^2 + 2.0 x + 3.0" + @test sprint(show, "text/plain", fc; context = :compact => true) == + "FuelCurve with power_units InfrastructureSystems.UnitSystemModule.UnitSystem.NATURAL_UNITS = 2, fuel_cost 4.0, vom_cost LinearCurve(0.0, 0.0), and value_curve:\n QuadraticCurve (a type of InfrastructureSystems.InputOutputCurve) where function is: f(x) = 1.0 x^2 + 2.0 x + 3.0" + + @test IS.get_power_units(cc) == IS.UnitSystem.NATURAL_UNITS + @test IS.get_power_units(fc) == IS.UnitSystem.NATURAL_UNITS + @test IS.get_power_units( + IS.CostCurve(zero(IS.InputOutputCurve), IS.UnitSystem.SYSTEM_BASE), + ) == + IS.UnitSystem.SYSTEM_BASE + @test IS.get_power_units( + IS.FuelCurve(zero(IS.InputOutputCurve), IS.UnitSystem.DEVICE_BASE, 1.0), + ) == + IS.UnitSystem.DEVICE_BASE + + @test IS.get_vom_cost(cc) == IS.LinearCurve(0.0) + @test IS.get_vom_cost(fc) == IS.LinearCurve(0.0) + @test IS.get_vom_cost( + IS.CostCurve(zero(IS.InputOutputCurve), IS.LinearCurve(1.0, 2.0)), + ) == + IS.LinearCurve(1.0, 2.0) + @test IS.get_vom_cost( + IS.FuelCurve(zero(IS.InputOutputCurve), 1.0, IS.LinearCurve(3.0, 4.0)), + ) == + IS.LinearCurve(3.0, 4.0) +end