Skip to content

Commit

Permalink
Merge pull request #392 from NREL-Sienna/gks/costs_to_is
Browse files Browse the repository at this point in the history
Move `ValueCurve`s, cost aliases, `CostCurve`, `FuelCurve`, and associated tests from PSY to IS (IS version)
  • Loading branch information
jd-lara authored Aug 3, 2024
2 parents 9be556c + b996e91 commit 3ce0e20
Show file tree
Hide file tree
Showing 5 changed files with 967 additions and 0 deletions.
7 changes: 7 additions & 0 deletions src/InfrastructureSystems.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down
199 changes: 199 additions & 0 deletions src/cost_aliases.jl
Original file line number Diff line number Diff line change
@@ -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
149 changes: 149 additions & 0 deletions src/production_variable_cost_curve.jl
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 3ce0e20

Please sign in to comment.