#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Standard Library Imports
from collections import defaultdict
# C Imports
import numpy as np
# ----------------------------------------------------------------------------
# SYSTEM OPERATOR
# ----------------------------------------------------------------------------
[docs]class SystemOperator(object):
"""System Operator
This is a container which contains all of the information about the
System. This includes demand data, nodes, stations etc.
It must be created first when developing a simulation.
Once it has been created it is passed to the creation of any other
object upon which it will automatically update itself.
Once the simulation has been fully defined it is passed to the
SPDmodel instance which draws all of the necessary variables
and values. These are taken and a Linear Program created which
is solved.
Usage:
------
operator = SystemOperator()
"""
def __init__(self):
super(SystemOperator, self).__init__()
self._create_empty_variables()
[docs] def create_iterator(self, actor=None, variable='reserve_price',
varrange=np.arange(0, 5)):
""" Create a range of duplicate scenarios which are all solved
at once to assess the benefits of a particular strategy over
a particular run.
This is a user exposed function and must be called whenever a
run is being created.
Parameters
----------
actor: Node, Station, InterruptibleLoad
An object which is to be modified when solving the linear program
variable: str
The name of the variable to be modified, e.g. 'reserve_price'
varrange: iterable
An iterable of ints or floats which consist of the new values
for the variable in each instance
"""
self.itinstances = []
self.itdispatches = {}
if actor:
for value in varrange:
itname = ''.join([variable, str(value)])
self.itinstances.append(itname)
actor.__dict__[variable] = value
self._add_dispatch(itname)
else:
# Do a single dispatch
itname = "Single"
self.itinstances.append(itname)
self._add_dispatch(itname)
return self
def _add_dispatch(self, itname):
""" Convenience wrapper, calls each of the parameter functons
Acts as a hidden API.
Parameters
----------
itname: str
The iterable name to be applied
"""
self._station_parameters(itname)
self._interruptible_load_parameters(itname)
self._node_parameters(itname)
self._transmission_parameters(itname)
self._rezerve_zone_parameters(itname)
def _station_parameters(self, itname):
""" Hidden function will create a number of lists and dictionaries
containing information about the Linear Program to be passed
to the model.
"""
for station in self.stations:
name = '_'.join([itname, station.name])
self.energy_station_names.append(name)
self.energy_station_capacity[name] = station.energy_offer
self.energy_station_price[name] = station.energy_price
self.reserve_station_names.append(name)
self.reserve_station_capacity[name] = station.reserve_offer
self.reserve_station_price[name] = station.reserve_price
self.reserve_station_proportion[name] = station.reserve_proportion
self.reserve_spinning_stations.append(name)
self.total_station_capacity[name] = station.capacity
def _interruptible_load_parameters(self, itname):
""" Hidden function will create a number of lists and dictionaries
containing information about the Linear Program to be passed
to the model.
"""
for IL in self.interruptible_loads:
name = '_'.join([itname, IL.name])
self.reserve_IL_names.append(name)
self.reserve_IL_capacity[name] = IL.reserve_offer
self.reserve_IL_price[name] = IL.reserve_price
self.reserve_station_names.append(name)
self.reserve_station_price[name] = IL.reserve_price
self.reserve_station_capacity[name] = IL.reserve_offer
def _node_parameters(self, itname):
""" Hidden function will create a number of lists and dictionaries
containing information about the Linear Program to be passed
to the model.
"""
for node in self.nodes:
name = '_'.join([itname, node.name])
self.node_names.append(name)
self.nodal_demand[name] = node.demand
# Nodal Stations
for station in node.stations:
stat_name = '_'.join([itname, station.name])
self.nodal_stations[name].append(stat_name)
def _transmission_parameters(self, itname):
""" Hidden function will create a number of lists and dictionaries
containing information about the Linear Program to be passed
to the model.
"""
for branch in self.branches:
name = '_'.join([itname, branch.name])
sn_name = '_'.join([itname, branch.sending_node.name])
rn_name = '_'.join([itname, branch.receiving_node.name])
self.branch_names.append(name)
self.node_flow_map[sn_name].append(name)
self.node_flow_map[rn_name].append(name)
self.branch_capacity[name] = branch.capacity
self.node_flow_direction[sn_name][name] = 1
self.node_flow_direction[rn_name][name] = -1
if branch.risk:
sn_rz_name = '_'.join([itname, branch.sending_node.RZ.name])
rn_rz_name = '_'.join([itname, branch.receiving_node.RZ.name])
if sn_rz_name != rn_rz_name:
self.reserve_zone_flow_map[sn_rz_name].append(name)
self.reserve_zone_flow_map[rn_rz_name].append(name)
self.reserve_zone_flow_direction[sn_rz_name][name] = 1
self.reserve_zone_flow_direction[rn_rz_name][name] = -1
def _rezerve_zone_parameters(self, itname):
""" Hidden function will create a number of lists and dictionaries
containing information about the Linear Program to be passed
to the model.
"""
for rz in self.reserve_zones:
name = '_'.join([itname, rz.name])
self.reserve_zone_names.append(name)
for station in rz.stations:
stat_name = '_'.join([itname, station.name])
self.reserve_zone_reserve[name].append(stat_name)
self.reserve_zone_generators[name].append(stat_name)
for il in rz.interruptible_loads:
il_name = '_'.join([itname, il.name])
self.reserve_zone_reserve[name].append(il_name)
def _create_empty_variables(self):
""" Initialises a number of empty lists and dictionaries
which are used in setting up the linear program
"""
self.stations = []
self.station_names = []
self.station_map = {}
self.energy_station_names = []
self.reserve_station_names = []
self.energy_station_price = {}
self.energy_station_capacity = {}
self.reserve_station_price = {}
self.reserve_station_proportion = {}
self.reserve_station_capacity = {}
self.total_station_capacity = {}
self.nodes = []
self.node_names = []
self.node_map = {}
self.node_flow_direction = defaultdict(dict)
self.node_flow_map = defaultdict(list)
self.nodal_stations = defaultdict(list)
self.nodal_demand = {}
self.reserve_zones = []
self.reserve_zone_names = []
self.reserve_zone_generators = defaultdict(list)
self.reserve_zone_reserve = defaultdict(list)
self.reserve_zone_transmission = defaultdict(list)
self.reserve_zone_flow_map = defaultdict(list)
self.reserve_zone_flow_direction = defaultdict(dict)
self.reserve_spinning_stations = []
self.interruptible_loads = []
self.interruptible_load_names = []
self.interruptible_load_map = {}
self.reserve_IL_names = []
self.reserve_IL_capacity = {}
self.reserve_IL_price = {}
self.branches = []
self.branch_names = []
self.branch_map = {}
self.branch_capacity = {}
return self
def _add_station(self, Station):
""" Adds a station automatically to the System Operator """
self.stations.append(Station)
return self
def _add_node(self, Node):
""" Adds a node automatically to the System Operator"""
self.nodes.append(Node)
return self
def _add_reserve_zone(self, RZ):
""" Adds a Reserve Zone automatically to the System Operator """
self.reserve_zones.append(RZ)
return self
def _add_interruptible_load(self, IL):
""" Adds a Interruptible Load participant to the System Operator """
self.interruptible_loads.append(IL)
return self
def _add_branch(self, Branch):
""" Adds a Branch to the Systen Operator """
self.branches.append(Branch)
return self
# ----------------------------------------------------------------------------
# PARTICIPANT CLASSES
# ----------------------------------------------------------------------------
[docs]class Company(object):
"""Company
A container around a number of Stations and Interruptible Load which
can be used to determine the aggregate position for a particular company.
This is useful when determining profits and losses from a particular
solution ot the model and should speed up the iteration process.
"""
def __init__(self, name):
super(Company, self).__init__()
self.name = name
self.stations = []
self.interruptible_loads = []
def _add_station(self, Station):
""" Automatically add a station to the Company.
Is called when a Station is created.
Parameters
----------
Station: Station
The station object to be added
"""
self.stations.append(Station)
return self
def _add_interruptible_load(self, IL):
""" Automatically add an interruptible load to the Company.
Is called when an interruptible load object is created
Parameters
----------
IL: InterruptibleLoad
The interruptible load object to be added
"""
self.interruptible_loads.append(IL)
return self
[docs]class Node(object):
"""Node
A nodal location within the current system.
Is part of a reserve zone and acts as a location for demand, generation
and reserve. Has a number of automatic methods which are called whenever
a new object is created at the node in question to handle the book
keeping operations.
Parameters
----------
name: str
A unique name for the Node
SO: SystemOperator
The system opeartor object
RZ: ReserveZone
What reserve zone the node is a part of
demand: int, float, default 0
The nodal demand at the node
"""
def __init__(self, name, SO, RZ, demand=0):
super(Node, self).__init__()
self.name = name
self.demand = demand
self.stations = []
self.interruptible_loads = []
RZ._add_node(self)
self.RZ = RZ
SO._add_node(self)
self.SO = SO
def _add_station(self, Station):
""" Automatically add a station to both the Node and the Reserve Zone
Parameters
----------
Station: Station
The station object to be added
"""
self.stations.append(Station)
self.RZ._add_station(Station)
return self
def _add_interruptible_load(self, IL):
""" Automatically add an interruptible load to the Node and
Reserve Zone.
Parameters
----------
IL: InterruptibleLoad
The interruptible load object to be added
"""
self.interruptible_loads.append(IL)
self.RZ._add_intload(IL)
return self
[docs]class ReserveZone(object):
"""ReserveZone
A Reserve Zone is a collection of nodes which have a separate "risk" which
must be secured against by dispatching reserve from the nodes within the
zone. Reserve procured from other zones cannot currently be utilised
to secure a risk in a separate zone
Parameters
----------
name: str
Unique name for the Reserve Zone
SO: SystemOperator
The System Operator object for the dispatch
"""
def __init__(self, name, SO):
super(ReserveZone, self).__init__()
self.name = name
self.nodes = []
self.stations = []
self.interruptible_loads = []
self.SO = SO
SO._add_reserve_zone(self)
def _add_node(self, Node):
""" Adds a node automatically to the Reserve Zone.
Is called automatically whenever a new node is created
Parameters
----------
Node: Node
The node to be added to the reserve zone
"""
self.nodes.append(Node)
return self
def _add_station(self, Station):
""" Adds a station automatically to the Reserve Zone.
Is called automatically when a new station is created
Parameters
----------
Station: Station
The station to be added to the reserve zone
"""
self.stations.append(Station)
return self
def _add_intload(self, IL):
""" Adds an interruptible load provider to the Rezerve Zone
Is called automatically when a new station is created
Parameters
----------
IL: InterruptibleLoad
The interruptible load object to be added to the reserve zone.
"""
self.interruptible_loads.append(IL)
return self
[docs]class Station(object):
"""Station
A generation station for use in the SPD model.
Is a container around the core functionality that can be called
automatically by the operator to provide its offers.
Ideally is a self contained agent that communicates with the SystemOperator
Parameters
----------
name: str
Unique name for the Generation Station
SO: SystemOperator
Operator object for the situation
Node: Node
Location of the generation station
Company: Company
Owner of the generation station for determining total revenue etc
capacity: int, float, default 0
Total generation capacity of the station
"""
def __init__(self, name, SO, Node, Company, capacity=0):
super(Station, self).__init__()
self.name = name
self.node = Node
self.company = Company
self.capacity = capacity
Node._add_station(self)
Company._add_station(self)
self.SO = SO
SO._add_station(self)
[docs] def add_energy_offer(self, price, offer):
""" Adds an Energy Offer to the station
Parameters
----------
price: int, float
The price of the Energy Offer
offer: int, float
Offer component of the Energy Offer
"""
self.energy_price = price
self.energy_offer = offer
return self
[docs] def add_reserve_offer(self, price, offer, proportion):
""" Adds a Reserve Offer to the Station
Parameters
----------
price: int, float
The price of the Reserve Offer
offer: int, float
Offer component of the Reserve Offer
proportion: float
Proportion component of the Reserve Offer
"""
self.reserve_price = price
self.reserve_offer = offer
self.reserve_proportion = proportion
return self
[docs]class InterruptibleLoad(object):
"""InterruptibleLoad
Container for an Interruptible Load participant within the market.
This participant acts as a non-generator source of reserve for
supporting a higher level of risk in the market.
Currently set up by passing a unique name to the object.
Parameters
----------
name: str
The unique name to be applied to the Interruptible Load object
SO: SystemOperator
The System Operator object
Node: Node
The location of the source of interruptible load
Company: Company
Who controls the Interruptible Load object, used when determining
profits or losses
"""
def __init__(self, name, SO, Node, Company):
""" Initialise the interruptible load object"""
super(InterruptibleLoad, self).__init__()
self.name = name
self.node = Node
self.company = Company
Node._add_interruptible_load(self)
Company._add_interruptible_load(self)
SO._add_interruptible_load(self)
[docs] def add_reserve_offer(self, price, offer):
""" Add a Reserve Offer to the object consisting of a price and offer
Parameters
----------
price: int, float
The price of the offer
offer: int, float
The quantity of the offer
"""
self.reserve_price = price
self.reserve_offer = offer
return self
[docs]class Branch(object):
"""Branch
A Branch is a connection point between any two nodes and specifies
the capacity between the nodes.
A branch may be a risk setting object, if the risk flag is set to True
If this is the case then the current implementation of the model requires
the sending and receiving node to be in different Reserve Zones.
To initiate a Branch object a minimum of three items must be passed.
Parameters
----------
SO: SystemOperator
The System Operator object for the current solution run
sending_node: Node
The node which is specified as the sending node for the model dispatch
receiving_node: Node
The receiving node for the model dispatch
capacity: int, float, default 0
The capacity of the branch
risk: bool, default False
Flag to treat the branch as a risk setting object.
"""
def __init__(self, SO, sending_node, receiving_node, capacity=0,
risk=False):
super(Branch, self).__init__()
SO._add_branch(self)
# Add the nodes
self.sending_node = sending_node
self.receiving_node = receiving_node
self.capacity = capacity
self.name = '_'.join([sending_node.name, receiving_node.name])
self.risk = risk
if __name__ == '__main__':
pass