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

RFC: subtype System to enable modelling alternative market clearing formulations #34

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
150 changes: 140 additions & 10 deletions src/system.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const MARKET_WIDE_ZONE = -9999
const BidName = InlineString31
const ZoneNum = Int64
const PriceSensitiveBid = Vector{Tuple{Float64, Float64}}
Copy link
Member

Choose a reason for hiding this comment

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

We should probably document what the tuple members represent


"""
$TYPEDEF
Expand All @@ -24,6 +25,14 @@ Base.@kwdef struct Zone
end
const Zones = Dictionary{ZoneNum, Zone}

Base.@kwdef struct FourRequirements
number::ZoneNum
regulation_up::Float64
regulation_down::Float64
responsive_regulation::Float64
non_spinning::Float64
end

const UnitCode = Int64
"""
$TYPEDEF
Expand Down Expand Up @@ -236,6 +245,10 @@ end

###### Time Series types ######

const AncillaryServiceTypes = Union{
KeyedArray{Union{Missing, Float64}, 2},
KeyedArray{Union{Missing, PriceSensitiveBid}, 2}
}
"""
$TYPEDEF

Expand All @@ -249,11 +262,11 @@ Base.@kwdef struct GeneratorTimeSeries
"Generation of the generator at the start of the time period (pu)"
Copy link
Member Author

Choose a reason for hiding this comment

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

This type may also need additional fields for multi_hour time series data. The offer curves don't appear to have multi hour flags but the ancillary services do. If the distribution of multi hours flags for a given day and resource can be different for the different services, we would need four multi hour time series, one for each of the ancillary services.

initial_generation::KeyedArray{Float64, 1}
"Generator offer curves. `KeyedArray` where the axis keys are `generator names x datetimes`"
offer_curve::KeyedArray{Vector{Tuple{Float64, Float64}}, 2}
offer_curve::KeyedArray{PriceSensitiveBid, 2}
"Generator minimum output in the ancillary services market (pu)"
regulation_min::KeyedArray{Float64, 2}
regulation_min::Union{Missing, KeyedArray{Float64, 2}}
"Generator maximum output in the ancillary services market (pu)"
regulation_max::KeyedArray{Float64, 2}
regulation_max::Union{Missing, KeyedArray{Float64, 2}}
Copy link
Member Author

Choose a reason for hiding this comment

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

regulation_min and regulation_max do not exist as parameters for modelling the new market so these fields can be missing.

Copy link
Member

Choose a reason for hiding this comment

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

If we intend to reuse code, is it easier to set the values to something like Inf rather than checking for missings?

Copy link
Member Author

Choose a reason for hiding this comment

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

Interesting question, this is how regulation_max is used in FNModels. I think if we set it to Inf and tried to define that constraint, that wouldn't give us the intended behaviour?

Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure but it looks like it would work out to a constraint that is always true? We might have to reformat the expression or add an extra constraint but it might be worth it to avoid having to special-case things to account for possible missings.

"Generator minimum output (pu)"
pmin::KeyedArray{Float64, 2}
"Generator maximum output (pu)"
Expand All @@ -262,22 +275,57 @@ Base.@kwdef struct GeneratorTimeSeries
Ancillary services regulation reserve offer prices (\$ /pu).
Generators not providing the service will have `missing` offer data.
"""
regulation_offers::KeyedArray{Union{Missing, Float64}, 2}
regulation_offers::AncillaryServiceTypes
"""
Ancillary services spinning reserve offer prices (\$ /pu).
Generators not providing the service will have `missing` offer data.
"""
spinning_offers::KeyedArray{Union{Missing, Float64}, 2}
spinning_offers::AncillaryServiceTypes
"""
Ancillary services online supplemental reserve offer prices (\$ /pu).
Generators not providing the service will have `missing` offer data.
"""
on_supplemental_offers::KeyedArray{Union{Missing, Float64}, 2}
on_supplemental_offers::AncillaryServiceTypes
"""
Ancillary services offline supplemental reserve offer prices (\$ /pu).
Generators not providing the service will have `missing` offer data.
"""
off_supplemental_offers::KeyedArray{Union{Missing, Float64}, 2}
off_supplemental_offers::AncillaryServiceTypes

"""
Ancillary services regulation reserve offer prices (\$ /pu).
Generators not providing the service will have `missing` offer data.
Some market clearing formulations do not include this service, so the field will be set
to `missing` by default.
"""
regulation_down_offers::Union{Missing, AncillaryServiceTypes} = missing
end

function GeneratorTimeSeries(
initial_generation,
offer_curve,
regulation_min,
regulation_max,
pmin,
pmax,
regulation_offers,
spinning_offers,
on_supplemental_offers,
off_supplemental_offers,
)
return GeneratorTimeSeries(
initial_generation,
offer_curve,
regulation_min,
regulation_max,
pmin,
pmax,
regulation_offers,
spinning_offers,
on_supplemental_offers,
off_supplemental_offers,
missing
)
end

"""
Expand Down Expand Up @@ -319,6 +367,31 @@ Base.@kwdef struct GeneratorStatusRT <: GeneratorStatus
regulation_commitment::KeyedArray{Bool, 2}
end

struct LoadServices
pmin::KeyedArray{Float64, 2}
pmax::KeyedArray{Float64, 2}
regulation_up_offers::KeyedArray{Union{Missing, PriceSensitiveBid}, 2}
regulation_down_offers::KeyedArray{Union{Missing, PriceSensitiveBid}, 2}
spinning_offers::KeyedArray{Union{Missing, PriceSensitiveBid}, 2}
on_supplemental_offers::KeyedArray{Union{Missing, PriceSensitiveBid}, 2}
off_supplemental_offers::KeyedArray{Union{Missing, PriceSensitiveBid}, 2}
end

struct EnergyBids
Copy link
Member Author

Choose a reason for hiding this comment

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

Is this a good name? When we refer to "bids" it can mean bids in either direction. But in the market data "bid" means demand and "offer" means supply.

"""
Bool indicating whether the bid is part of a multi-hour block, where either all bids
must clear or none do.
"""
multi_hour::KeyedArray{Bool, 2}
"""
Bool indicating whether the bid is a fixed type which can only clear at the given price
and volume.
"""
is_fixed_type::KeyedArray{Bool, 2}
Copy link
Member Author

Choose a reason for hiding this comment

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

Is a Boolean going to be sufficient here? There are three types of bid: curve, variable and fixed. In terms of how the formulation is implemented I think variable is just a curve with only one block. In which case we just have to distinguish which ones are fixed.

"Bid curve, `KeyedArray` where the axis keys are `generator names x datetimes`."
bid_curve::KeyedArray{PriceSensitiveBid, 2}
end

"""
System

Expand Down Expand Up @@ -387,11 +460,68 @@ Base.@kwdef mutable struct SystemDA <: System

# Virtuals/PSD time series
"Increment bids time series data. `KeyedArray` where the axis keys are `bid ids x datetimes`"
increments::KeyedArray{Vector{Tuple{Float64, Float64}}, 2}
increments::KeyedArray{PriceSensitiveBid, 2}
"Decrement bids time series data. `KeyedArray` where the axis keys are `bid ids x datetimes`"
decrements::KeyedArray{Vector{Tuple{Float64, Float64}}, 2}
decrements::KeyedArray{PriceSensitiveBid, 2}
"Price sensitive load bids time series data. `KeyedArray` where the axis keys are `bid ids x datetimes`"
price_sensitive_loads::KeyedArray{Vector{Tuple{Float64, Float64}}, 2}
price_sensitive_loads::KeyedArray{PriceSensitiveBid, 2}
end

"""
$TYPEDEF

Subtype of a `System` for modelling a purely financial style of day-ahead market clearing.
Key differences to the physical day-ahead market clearing approach are that _all_ offers and
bids are price sensitive and virtual participants are not distinguished from physical
participants.

Fields:
$TYPEDFIELDS
"""
Base.@kwdef mutable struct FinancialSystemDA <: System
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure about this name since it clashes with another widely-used susbystem in our codebase that we are probably going to have to deal with eventually. Is there another appropriate name?

"`Dictionary` where the keys are bus names and the values are generator ids at that bus"
gens_per_bus::Dictionary{BusName, Vector{String}}
"`Dictionary` where the keys are bus names and the values are increment bid ids at that bus"
energy_offers_per_bus::Dictionary{BusName, Vector{BidName}}
Copy link
Member

Choose a reason for hiding this comment

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

Are these similar to incs_per_bus in the other systems? Should we try to keep the names consistent?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, I think they are identical in terms of type. It's a question of different concepts. incs/increments refer specifically to virtual bids, whereas here the energy_offers can be physical or virtual and we don't have a way to distinguish between the two.

Copy link
Member

Choose a reason for hiding this comment

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

Perhaps we should go with the most generic option then use a type to differentiate if we need to? If we use them similarly and they behave in a similar way that is.
It seems a shame to have duplicate code when we could use the same accessor across systems,

"`Dictionary` where the keys are bus names and the values are decrement bid ids at that bus"
energy_bids_per_bus::Dictionary{BusName, Vector{BidName}}
"`Dictionary` where the keys are bus names and the values are load ids at that bus"
load_resources_per_bus::Dictionary{BusName, Vector{BidName}}

"Zones in the `System`, which will also include a `Requirements` entry for the market wide zone"
zones::Dictionary{ZoneNum, FourRequirements}
"Buses in the `System` indexed by bus name"
buses::Buses
"Generators in the `System` indexed by unit code"
generators::Generators
"Branches in the `System` indexed by branch name"
branches::Branches
"""
The line outage distribution factor matrix of the system for a set of contingencies given
by the keys of the `Dictionary`. Each entry is a `KeyedArray` with axis keys
`branch names x branch on outage`
"""
lodfs::Dictionary{String, KeyedArray{Float64, 2}}
"""
Power transfer distribution factor of the system. `KeyedArray` where the axis keys are
`branch names x bus names`
"""
ptdf::Union{KeyedArray{Float64, 2}, Missing}

# Generator related time series
"Generator related time series data"
generator_time_series::GeneratorTimeSeries
"Generator status time series needed for the day-ahead formulation"
generator_status::GeneratorStatusDA

# Load time series
"Load time series data. `KeyedArray` where the axis keys are `load ids x datetimes`"
load_services::LoadServices

"Energy offers time series data. `KeyedArray` where the axis keys are `bid ids x datetimes`"
energy_offers::EnergyBids
Copy link
Member

Choose a reason for hiding this comment

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

Should we switch the other systems to use EnergyBids as well? It looks like we should be able to infer the extra information required

Copy link
Member Author

Choose a reason for hiding this comment

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

I think that could work. I suppose there would be some (minimal?) overhead to storing the extra information, but the advantages of reusable code.

Copy link
Member

Choose a reason for hiding this comment

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

Since it's repeated information and is generally read-only we could probably use an efficient data structure to avoid too much extra storage

"Energy demands time series data. `KeyedArray` where the axis keys are `bid ids x datetimes`"
energy_demands::EnergyBids
Copy link
Member Author

Choose a reason for hiding this comment

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

Do these field names make sense? Previously we have referred to increment and decrement bids but these EnergyBids are not necessarily virtual, so do "energy_offers" and "energy_demands" make sense?

Copy link
Member

Choose a reason for hiding this comment

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

I think the names make sense but since we're already explaining things in terms of "supply" and "demand" to clarify, perhaps those are the more straightforward terms to use? Or will all markets use these terms consistently?

end

"""
Expand Down