Skip to content

Commit

Permalink
Merge pull request #331 from NREL-Sienna/gks/variable_cost_refactor
Browse files Browse the repository at this point in the history
Variable Cost Refactor Part 1: Function Data
  • Loading branch information
jd-lara authored Feb 22, 2024
2 parents 1500174 + dff57ce commit 7f0d636
Show file tree
Hide file tree
Showing 10 changed files with 632 additions and 410 deletions.
1 change: 1 addition & 0 deletions src/InfrastructureSystems.jl
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ include("utils/generate_structs.jl")
include("utils/lazy_dict_from_iterator.jl")
include("utils/logging.jl")
include("utils/stdout_redirector.jl")
include("function_data.jl")
include("utils/utils.jl")
include("internal.jl")
include("time_series_storage.jl")
Expand Down
2 changes: 0 additions & 2 deletions src/common.jl
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,4 @@ struct HashMismatchError <: Exception
end

const CONSTANT = Float64
const POLYNOMIAL = Tuple{Float64, Float64}
const PWL = Vector{Tuple{Float64, Float64}}
const LIM_TOL = 1e-6
33 changes: 10 additions & 23 deletions src/deterministic.jl
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
"""
mutable struct Deterministic <: AbstractDeterministic
name::String
data::Union{
SortedDict{Dates.DateTime, Vector{CONSTANT}},
SortedDict{Dates.DateTime, Vector{POLYNOMIAL}},
SortedDict{Dates.DateTime, Vector{PWL}},
}
data::SortedDict
resolution::Dates.Period
scaling_factor_multiplier::Union{Nothing, Function}
internal::InfrastructureSystemsInternal
Expand All @@ -16,7 +12,7 @@ A deterministic forecast for a particular data field in a Component.
# Arguments
- `name::String`: user-defined name
- `data::Union{SortedDict{Dates.DateTime, Vector{CONSTANT}}, SortedDict{Dates.DateTime, Vector{POLYNOMIAL}}, SortedDict{Dates.DateTime, Vector{PWL}}}`: timestamp - scalingfactor
- `data::SortedDict`: timestamp - scalingfactor
- `resolution::Dates.Period`: forecast resolution
- `scaling_factor_multiplier::Union{Nothing, Function}`: Applicable when the time series
data are scaling factors. Called on the associated component to convert the values.
Expand All @@ -26,11 +22,7 @@ mutable struct Deterministic <: AbstractDeterministic
"user-defined name"
name::String
"timestamp - scalingfactor"
data::Union{
SortedDict{Dates.DateTime, Vector{CONSTANT}},
SortedDict{Dates.DateTime, Vector{POLYNOMIAL}},
SortedDict{Dates.DateTime, Vector{PWL}},
}
data::SortedDict # TODO handle typing here in a more principled fashion
"forecast resolution"
resolution::Dates.Period
"Applicable when the time series data are scaling factors. Called on the associated component to convert the values."
Expand Down Expand Up @@ -202,19 +194,13 @@ function Deterministic(forecast::Deterministic, data)
return Deterministic(; vals...)
end

convert_data(data::AbstractDict{Dates.DateTime, Vector{T}}) where {T} =
SortedDict{Dates.DateTime, Vector{CONSTANT}}(data...)
convert_data(data::AbstractDict{Dates.DateTime, Vector{T}}) where {T <: Tuple} =
SortedDict{Dates.DateTime, Vector{POLYNOMIAL}}(data...)
convert_data(data::AbstractDict{Dates.DateTime, Vector{Vector{T}}}) where {T <: Tuple} =
SortedDict{Dates.DateTime, Vector{PWL}}(data...)
convert_data(
data::Union{
SortedDict{Dates.DateTime, Vector{CONSTANT}},
SortedDict{Dates.DateTime, Vector{POLYNOMIAL}},
SortedDict{Dates.DateTime, Vector{PWL}},
},
) = data
data::AbstractDict{<:Any, Vector{T}},
) where {T <: Union{CONSTANT, FunctionData}} =
SortedDict{Dates.DateTime, Vector{T}}(data...)
convert_data(
data::SortedDict{Dates.DateTime, Vector{T}},
) where {T <: Union{CONSTANT, FunctionData}} = data

# Workaround for a bug/limitation in SortedDict. If a user tries to construct
# SortedDict(i => ones(2) for i in 1:2)
Expand Down Expand Up @@ -271,6 +257,7 @@ Set [`Deterministic`](@ref) `internal`.
"""
set_internal!(value::Deterministic, val) = value.internal = val

# TODO handle typing here in a more principled fashion
eltype_data(forecast::Deterministic) = eltype_data_common(forecast)
get_count(forecast::Deterministic) = get_count_common(forecast)
get_horizon(forecast::Deterministic) = get_horizon_common(forecast)
Expand Down
222 changes: 222 additions & 0 deletions src/function_data.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
abstract type FunctionData end

"""
Structure to represent the underlying data of linear functions. Principally used for
the representation of cost functions `f(x) = proportional_term*x`.
# Arguments
- `proportional_term::Float64`: the proportional term in the function
`f(x) = proportional_term*x`
"""
struct LinearFunctionData <: FunctionData
proportional_term::Float64
end

get_proportional_term(fd::LinearFunctionData) = fd.proportional_term

"""
Structure to represent the underlying data of quadratic polynomial functions. Principally
used for the representation of cost functions
`f(x) = quadratic_term*x^2 + proportional_term*x + constant_term`.
# Arguments
- `quadratic_term::Float64`: the quadratic term in the represented function
- `proportional_term::Float64`: the proportional term in the represented function
- `constant_term::Float64`: the constant term in the represented function
"""
struct QuadraticFunctionData <: FunctionData
quadratic_term::Float64
proportional_term::Float64
constant_term::Float64
end

get_quadratic_term(fd::QuadraticFunctionData) = fd.quadratic_term
get_proportional_term(fd::QuadraticFunctionData) = fd.proportional_term
get_constant_term(fd::QuadraticFunctionData) = fd.constant_term

"""
Structure to represent the underlying data of higher order polynomials. Principally used for
the representation of cost functions where
`f(x) = sum_{i in keys(coefficients)} coefficients[i]*x^i`.
# Arguments
- `coefficients::Dict{Int, Float64}`: values are coefficients, keys are degrees to which
the coefficients apply (0 for the constant term, 2 for the squared term, etc.)
"""
struct PolynomialFunctionData <: FunctionData
coefficients::Dict{Int, Float64}
end

get_coefficients(fd::PolynomialFunctionData) = fd.coefficients

function _validate_piecewise_x(x_coords)
# TODO currently there exist cases where we are constructing a PiecewiseLinearPointData
# with only one point (e.g., `calculate_variable_cost` within
# `power_system_table_data.jl`) -- what does this represent?
# (length(x_coords) >= 2) ||
# throw(ArgumentError("Must specify at least two x-coordinates"))
issorted(x_coords) || throw(ArgumentError("Piecewise x-coordinates must be ascending"))
end

"""
Structure to represent pointwise piecewise linear data. Principally used for the
representation of cost functions where the points store quantities (x, y), such as (MW, \$).
The curve starts at the first point given, not the origin.
# Arguments
- `points::Vector{Tuple{Float64, Float64}}`: the points (x, y) that define the function
"""
struct PiecewiseLinearPointData <: FunctionData
points::Vector{Tuple{Float64, Float64}}

function PiecewiseLinearPointData(points)
_validate_piecewise_x(first.(points))
return new(points)
end
end

"Get the points that define the piecewise data"
get_points(data::PiecewiseLinearPointData) = data.points

"Get the x-coordinates of the points that define the piecewise data"
get_x_coords(data::PiecewiseLinearPointData) = first.(get_points(data))

function _get_slopes(vc::Vector{NTuple{2, Float64}})
slopes = Vector{Float64}(undef, length(vc) - 1)
(prev_x, prev_y) = vc[1]
for (i, (comp_x, comp_y)) in enumerate(vc[2:end])
slopes[i] = (comp_y - prev_y) / (comp_x - prev_x)
(prev_x, prev_y) = (comp_x, comp_y)
end
return slopes
end

_get_x_lengths(x_coords) = x_coords[2:end] .- x_coords[1:(end - 1)]

"""
Calculates the slopes of the line segments defined by the PiecewiseLinearPointData,
returning one fewer slope than the number of underlying points.
"""
function get_slopes(pwl::PiecewiseLinearPointData)
return _get_slopes(get_points(pwl))
end

"""
Structure to represent the underlying data of slope piecewise linear data. Principally used
for the representation of cost functions where the points store quantities (x, dy/dx), such
as (MW, \$/MW).
# Arguments
- `x_coords::Vector{Float64}`: the x-coordinates of the endpoints of the segments
- `y0::Float64`: the y-coordinate of the data at the first x-coordinate
- `slopes::Vector{Float64}`: the slopes of the segments: `slopes[1]` is the slope between
`x_coords[1]` and `x_coords[2]`, etc.
"""
struct PiecewiseLinearSlopeData <: FunctionData
x_coords::Vector{Float64}
y0::Float64
slopes::Vector{Float64}

function PiecewiseLinearSlopeData(x_coords, y0, slopes)
_validate_piecewise_x(x_coords)
(length(slopes) == length(x_coords) - 1) ||
throw(ArgumentError("Must specify one fewer slope than x-coordinates"))
return new(x_coords, y0, slopes)
end
end

"Get the slopes that define the PiecewiseLinearSlopeData"
get_slopes(data::PiecewiseLinearSlopeData) = data.slopes

"Get the x-coordinates of the points that define the piecewise data"
get_x_coords(data::PiecewiseLinearSlopeData) = data.x_coords

"Get the y-coordinate of the data at the first x-coordinate"
get_y0(data::PiecewiseLinearSlopeData) = data.y0

"Calculate the endpoints of the segments in the PiecewiseLinearSlopeData"
function get_points(data::PiecewiseLinearSlopeData)
slopes = get_slopes(data)
x_coords = get_x_coords(data)
points = Vector{Tuple{Float64, Float64}}(undef, length(x_coords))
running_y = get_y0(data)
points[1] = (x_coords[1], running_y)
for (i, (prev_slope, this_x, dx)) in
enumerate(zip(slopes, x_coords[2:end], get_x_lengths(data)))
running_y += prev_slope * dx
points[i + 1] = (this_x, running_y)
end
return points
end

"""
Calculates the x-length of each segment of a piecewise curve.
"""
function get_x_lengths(
pwl::Union{PiecewiseLinearPointData, PiecewiseLinearSlopeData},
)
return _get_x_lengths(get_x_coords(pwl))
end

Base.length(pwl::Union{PiecewiseLinearPointData, PiecewiseLinearSlopeData}) =
length(get_x_coords(pwl)) - 1

Base.getindex(pwl::PiecewiseLinearPointData, ix::Int) =
getindex(get_points(pwl), ix)

Base.:(==)(a::PiecewiseLinearPointData, b::PiecewiseLinearPointData) =
get_points(a) == get_points(b)

Base.:(==)(a::PiecewiseLinearSlopeData, b::PiecewiseLinearSlopeData) =
(get_x_coords(a) == get_x_coords(b)) &&
(get_y0(a) == get_y0(b)) &&
(get_slopes(a) == get_slopes(b))

Base.:(==)(a::PolynomialFunctionData, b::PolynomialFunctionData) =
get_coefficients(a) == get_coefficients(b)

function _slope_convexity_check(slopes::Vector{Float64})
for ix in 1:(length(slopes) - 1)
if slopes[ix] > slopes[ix + 1]
@debug slopes
return false
end
end
return true
end

"""
Returns True/False depending on the convexity of the underlying data
"""
is_convex(pwl::Union{PiecewiseLinearPointData, PiecewiseLinearSlopeData}) =
_slope_convexity_check(get_slopes(pwl))

# kwargs-only constructors for deserialization
LinearFunctionData(; proportional_term) = LinearFunctionData(proportional_term)

QuadraticFunctionData(; quadratic_term, proportional_term, constant_term) =
QuadraticFunctionData(quadratic_term, proportional_term, constant_term)

PolynomialFunctionData(; coefficients) = PolynomialFunctionData(coefficients)

PiecewiseLinearPointData(; points) = PiecewiseLinearPointData(points)

PiecewiseLinearSlopeData(; x_coords, y0, slopes) =
PiecewiseLinearSlopeData(x_coords, y0, slopes)

serialize(val::FunctionData) = serialize_struct(val)

function deserialize_struct(T::Type{PolynomialFunctionData}, val::Dict)
data = deserialize_to_dict(T, val)
data[Symbol("coefficients")] =
Dict(
(k isa String ? parse(Int, k) : k, v)
for (k, v) in data[Symbol("coefficients")]
)
return T(; data...)
end

deserialize(T::Type{<:FunctionData}, val::Dict) = deserialize_struct(T, val)

deserialize(::Type{FunctionData}, val::Dict) =
throw(ArgumentError("FunctionData is abstract, must specify a concrete subtype"))
Loading

0 comments on commit 7f0d636

Please sign in to comment.