Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move ValueCurves, cost aliases, CostCurve, FuelCurve, and associated tests from PSY to IS (IS version) #392

Merged
merged 4 commits into from
Aug 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor Author

@GabrielKS GabrielKS Aug 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jd-lara you won't love this, but exporting the aliases is the only way to get them to register as aliases for the purposes of printing and such, and I think overriding the printing of these types is uglier than adding them as exports. @daniel-thom agrees that this is fairly harmless — a name collision is unlikely because these would be imported rather than redefined in any package that also exports them, and also we are (possibly accidentally) already exporting some things from IS.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uggg. Fine

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
Loading