Skip to content

Commit

Permalink
Merge pull request #44 from PaulTalbot-INL/custom_disp
Browse files Browse the repository at this point in the history
Custom dispatch API
  • Loading branch information
dylanjm authored Oct 13, 2020
2 parents 8459a4f + 7d5a2d2 commit 2e84471
Show file tree
Hide file tree
Showing 21 changed files with 372 additions and 69 deletions.
32 changes: 13 additions & 19 deletions src/Cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,18 +126,11 @@ def get_input_specs(cls):

# dispatcher
dispatch = InputData.parameterInputFactory('dispatcher', ordered=False,
descr=r"""This node defines the dispatch strategy and options to use in the ``inner'' run.""")
dispatch_options = InputTypes.makeEnumType('DispatchOptions', 'DispatchOptionsType', [d for d in known_dispatchers])
dispatch.addSub(InputData.parameterInputFactory('type', contentType=dispatch_options,
descr=r"""the name of the ``inner'' dispatch strategy to use."""))
incr = InputData.parameterInputFactory('increment', contentType=InputTypes.FloatType,
descr=r"""When performing an incremental resource balance as part of a dispatch solve, this
determines the size of incremental adjustments to make for the given resource. If this
value is large, then the solve is accelerated, but may miss critical inflection points
in economical tradeoff. If this value is small, the solve may take much longer.""")
incr.addParam('resource', param_type=InputTypes.StringType, required=True,
descr=r"""indicates the resource for which this increment is being defined.""")
dispatch.addSub(incr)
descr=r"""This node defines the dispatch strategy and options to use in the ``inner''
run.""")
for d in known_dispatchers:
vld_spec = get_dispatcher(d).get_input_specs()
dispatch.addSub(vld_spec)
input_specs.addSub(dispatch)

# validator
Expand All @@ -151,7 +144,7 @@ def get_input_specs(cls):

return input_specs

def __init__(self, **kwargs):
def __init__(self, run_dir, **kwargs):
"""
Constructor
@ In, None
Expand All @@ -161,6 +154,7 @@ def __init__(self, **kwargs):
self.name = None # case name
self._mode = None # extrema to find: min, max, sweep
self._metric = 'NPV' # UNUSED (future work); economic metric to focus on: lcoe, profit, cost
self.run_dir = run_dir # location of HERON input file

self.dispatch_name = None # name of dispatcher to use
self.dispatcher = None # type of dispatcher to use
Expand All @@ -180,6 +174,9 @@ def __init__(self, **kwargs):
self._time_discretization = None # (start, end, number) for constructing time discretization, same as argument to np.linspace
self._Resample_T = None # user-set increments for resources

# clean up location
self.run_dir = os.path.abspath(os.path.expanduser(self.run_dir))

def read_input(self, xml):
"""
Sets settings from input file
Expand Down Expand Up @@ -209,14 +206,11 @@ def read_input(self, xml):
self._global_econ[sub.getName()] = sub.value
elif item.getName() == 'dispatcher':
# instantiate a dispatcher object.
name = item.findFirst('type').value
inp = item.subparts[0]
name = inp.getName()
typ = get_dispatcher(name)
self.dispatcher = typ()
self.dispatcher.read_input(item)
# XXX Remove -> send to dispatcher instead
for sub in item.subparts:
if item.getName() == 'increment':
self._increments[item.parameterValues['resource']] = item.value
self.dispatcher.read_input(inp)
elif item.getName() == 'validator':
vld = item.subparts[0]
name = vld.getName()
Expand Down
22 changes: 6 additions & 16 deletions src/Components.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,17 +212,14 @@ def get_resources(self):
res.update(self.get_outputs())
return res

def get_capacity(self, meta, raven_vars, dispatch, t, raw=False):
def get_capacity(self, meta, raw=False):
"""
returns the capacity of the interaction of this component
@ In, meta, dict, arbitrary metadata from EGRET
@ In, raven_vars, dict, evaluated RAVEN variables
@ In, dispatch, DispatchScenario.DispatchRecord, current dispatch situation
@ In, t, int, current time step
@ In, raw, bool, optional, if True then return the ValuedParam instance for capacity, instead of the evaluation
@ Out, capacity, float (or ValuedParam), the capacity of this component's interaction
"""
return self.get_interaction().get_capacity(meta, raven_vars, dispatch, t, raw=raw)
return self.get_interaction().get_capacity(meta, raw=raw)

def get_capacity_var(self):
"""
Expand Down Expand Up @@ -444,27 +441,20 @@ def _set_valued_param(self, name, comp, spec, mode):
self._crossrefs[name] = vp
setattr(self, name, vp)

def get_capacity(self, meta, raven_vars, dispatch, t, raw=False):
def get_capacity(self, meta, raw=False):
"""
Returns the capacity of this interaction.
Returns an evaluated value unless "raw" is True, then gives ValuedParam
@ In, meta, dict, additional variables to pass through
@ In, raven_vars, dict, TODO part of meta! consolidate!
@ In, dispatch, dict, TODO part of meta! consolidate!
@ In, t, int, TODO part of meta! consolidate!
@ In, raw, bool, optional, if True then provide ValuedParam instead of evaluation
@ Out, evaluated, float or ValuedParam, requested value
@ Out, meta, dict, additional variable passthrough
"""
if raw:
return self._capacity
request = {self._capacity_var: None}
inputs = {'request': request,
'meta': meta,
'raven_vars': raven_vars,
'dispatch': dispatch,
't': t}
evaluated, meta = self._capacity.evaluate(inputs, target_var=self._capacity_var)
meta['request'] = request
evaluated, meta = self._capacity.evaluate(meta, target_var=self._capacity_var)
return evaluated, meta

def get_capacity_var(self):
Expand Down Expand Up @@ -625,7 +615,7 @@ def _check_capacity_limit(self, res, amt, balance, meta, raven_vars, dispatch, t
@ Out, balance, dict, new results of requested action, possibly modified if capacity hit
@ Out, meta, dict, additional variable passthrough
"""
cap = self.get_capacity(meta, raven_vars, dispatch, t)[0][self._capacity_var]
cap = self.get_capacity(meta)[0][self._capacity_var]
try:
if abs(balance[self._capacity_var]) > abs(cap):
#ttttt
Expand Down
2 changes: 2 additions & 0 deletions src/DispatchManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,14 @@ def extract_variables(self, raven, raven_dict):

# TODO magic keywords (e.g. verbosity, MAX_TIMES, MAX_YEARS, ONLY_DISPATCH, etc)
# TODO other arbitrary constants, such as sampled values from Outer needed in Inner?

# component capacities
for comp in self._components:
name = self.naming_template['comp capacity'].format(comp=comp.name)
update_capacity = raven_dict.get(name) # TODO is this ever not provided?
comp.set_capacity(update_capacity)
# TODO other case, component properties

# load ARMA signals
for source in self._sources:
if source.is_type('ARMA'):
Expand Down
4 changes: 2 additions & 2 deletions src/ValuedParams.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,11 +360,11 @@ def _evaluate_variable(self, inputs, target_var, aliases):
if variable in aliases:
variable = aliases[variable]
try:
val = inputs['raven_vars'][variable]
val = inputs['HERON']['RAVEN_vars'][variable]
except KeyError as e:
print('ERROR: requested variable "{}" not found among RAVEN variables!'.format(variable))
print(' -> Available:')
for vn in inputs['raven_vars'].keys():
for vn in inputs['HERON']['RAVEN_vars'].keys():
print(' ', vn)
raise e
return {key: float(val)}
Expand Down
110 changes: 110 additions & 0 deletions src/dispatch/CustomDispatcher.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@

# Copyright 2020, Battelle Energy Alliance, LLC
# ALL RIGHTS RESERVED
"""
Interface for user-provided dispatching strategies.
"""
import os
import inspect
import numpy as np

from utils import utils, InputData, InputTypes
from .Dispatcher import Dispatcher
from .DispatchState import NumpyState

class Custom(Dispatcher):
"""
Base class for strategies for consecutive dispatching in a continuous period.
"""
# ---------------------------------------------
# INITIALIZATION
@classmethod
def get_input_specs(cls):
"""
Set acceptable input specifications.
@ In, None
@ Out, specs, InputData, specs
"""
specs = InputData.parameterInputFactory('custom', ordered=False, baseNode=None)
specs.addSub(InputData.parameterInputFactory('location', contentType=InputTypes.StringType,
descr=r"""The hard drive location of the custom dispatcher. Relative paths are taken with
respect to the HERON run location. Custom dispatchers must implement
a \texttt{dispatch} method that accepts the HERON case, components, and sources; this
method must return the activity for each resource of each component."""))
return specs

def __init__(self):
"""
Constructor.
@ In, None
@ Out, None
"""
Dispatcher.__init__(self)
self.name = 'CustomDispatcher'
self._usr_loc = None # user-provided path to custom dispatcher module
self._file = None # resolved pathlib.Path to the custom dispatcher module

def read_input(self, inputs):
"""
Loads settings based on provided inputs
@ In, inputs, InputData.InputSpecs, input specifications
@ Out, None
"""
usr_loc = inputs.findFirst('location')
if usr_loc is None:
raise RuntimeError('No <location> provided for <custom> dispatch strategy in <Case>!')
# assure python extension, omitting it is a user convenience
if not usr_loc.value.endswith('.py'):
usr_loc.value += '.py'
self._usr_loc = os.path.abspath(os.path.expanduser(usr_loc.value))

def initialize(self, case, components, sources, **kwargs):
"""
Initialize dispatcher properties.
@ In, case, Case, HERON case instance
@ In, components, list, HERON components
@ In, sources, list, HERON sources
@ In, kwargs, dict, keyword arguments
@ Out, None
"""
start_loc = case.run_dir
file_loc = os.path.abspath(os.path.join(start_loc, self._usr_loc))
# check that it exists
if not os.path.isfile(file_loc):
raise IOError(f'Custom dispatcher not found at "{file_loc}"! (input dir "{start_loc}", provided path "{self._usr_loc}"')
self._file = file_loc
print(f'Loading custom dispatch at "{self._file}"')
# load user module
load_string, _ = utils.identifyIfExternalModelExists(self, self._file, '')
module = utils.importFromPath(load_string, True)
# check it works as intended
if not 'dispatch' in dir(module):
raise IOError(f'Custom Dispatch at "{self._file}" does not have a "dispatch" method!')
# TODO other checks?

def dispatch(self, case, components, sources, meta):
"""
Performs technoeconomic dispatch.
@ In, case, Case, HERON case
@ In, components, list, HERON components
@ In, sources, list, HERON sources
@ Out, results, dict, economic and production metrics
"""
# load up time indices
t_start, t_end, t_num = self.get_time_discr()
time = np.linspace(t_start, t_end, t_num) # Note we don't care about segment/cluster here
# load user module
load_string, _ = utils.identifyIfExternalModelExists(self, self._file, '')
module = utils.importFromPath(load_string, True)
# run dispatch
results = module.dispatch(meta)
# load activity into a DispatchState
state = NumpyState()
indexer = meta['HERON']['resource_indexer']
state.initialize(components, indexer, time)
for comp in components:
for resource, activity in results.get(comp.name, {}).items():
state.set_activity_vector(comp, resource, 0, t_num, activity)
return state


8 changes: 2 additions & 6 deletions src/dispatch/Factory.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@

# Copyright 2020, Battelle Energy Alliance, LLC
# ALL RIGHTS RESERVED
# from generic import Generic
# from marginal import marginal
# from custom import Custom
from .pyomo_dispatch import Pyomo
from .CustomDispatcher import Custom

known = {
#'generic': Generic,
#'marginal': Marginal,
#'custom': Custom,
'pyomo': Pyomo,
'custom': Custom,
}

def get_class(typ):
Expand Down
11 changes: 7 additions & 4 deletions src/dispatch/pyomo_dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import pyomo.environ as pyo
from pyomo.opt import SolverStatus, TerminationCondition

from utils import InputData, InputTypes

# allows pyomo to solve on threaded processes
import pyutilib.subprocess.GlobalData
pyutilib.subprocess.GlobalData.DEFINE_SIGNAL_HANDLERS_DEFAULT = False
Expand Down Expand Up @@ -54,7 +56,7 @@ def get_input_specs(cls):
@ In, None
@ Out, specs, InputData, specs
"""
specs = InputData.parameterInputFactory('Dispatcher', ordered=False, baseNode=None)
specs = InputData.parameterInputFactory('pyomo', ordered=False, baseNode=None)
# TODO specific for pyomo dispatcher
return specs

Expand Down Expand Up @@ -155,7 +157,7 @@ def dispatch_window(self, time,
## same as other production variables, we create components with limitation
## lowerbound == upperbound == capacity (just for "fixed" dispatch components)
prod_name = self._create_production(m, comp) # variables
self._create_capacity(m, comp, prod_name) # capacity constraints
self._create_capacity(m, comp, prod_name, meta) # capacity constraints
self._create_transfer(m, comp, prod_name) # transfer functions (constraints)
# ramp rates TODO ## INCLUDING previous-time boundary condition TODO
self._create_conservation(m, resources, meta) # conservation of resources (e.g. production == consumption)
Expand Down Expand Up @@ -257,12 +259,13 @@ def _create_production(self, m, comp):
setattr(m, prod_name, prod)
return prod_name

def _create_capacity(self, m, comp, prod_name):
def _create_capacity(self, m, comp, prod_name, meta):
"""
Creates pyomo capacity constraints
@ In, m, pyo.ConcreteModel, associated model
@ In, comp, HERON Component, component to make variables for
@ In, prod_name, str, name of production variable
@ In, meta, dict, additional state information
@ Out, None
"""
name = comp.name
Expand All @@ -272,7 +275,7 @@ def _create_capacity(self, m, comp, prod_name):
## NOTE get_capacity returns (data, meta) and data is dict
## TODO does this work with, e.g., ARMA-based capacities?
### -> "time" is stored on "m" and could be used to correctly evaluate the capacity
cap = comp.get_capacity(None, None, None, None)[0][cap_res] # value of capacity limit (units of governing resource)
cap = comp.get_capacity(meta)[0][cap_res] # value of capacity limit (units of governing resource)
rule = partial(self._capacity_rule, prod_name, r, cap)
constr = pyo.Constraint(m.T, rule=rule)
setattr(m, '{c}_{r}_capacity_constr'.format(c=name, r=cap_res), constr)
Expand Down
2 changes: 1 addition & 1 deletion src/input_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def parse(xml, loc, messageHandler):

# intentionally read case first
case_node = xml.find('Case')
case = Cases.Case(messageHandler=messageHandler)
case = Cases.Case(loc, messageHandler=messageHandler)
case.read_input(case_node)

# read everything else
Expand Down
4 changes: 2 additions & 2 deletions templates/template_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ def _modify_outer_samplers(self, template, case, components):
## --> this really needs to be made generic for all kinds of valued params!
name = component.name
var_name = self.namingTemplates['variable'].format(unit=name, feature='capacity')
cap = interaction.get_capacity(None, None, None, None, raw=True)
cap = interaction.get_capacity(None, raw=True)
# do we already know the capacity values?
if cap.type == 'value':
vals = cap.get_values()
Expand Down Expand Up @@ -499,8 +499,8 @@ def _modify_inner_components(self, template, case, components):
## For each interaction of each component, that means making sure the Function, ARMA, or constant makes it.
## Constants from outer (namely sweep/opt capacities) are set in the MC Sampler from the outer
## The Dispatch needs info from the Outer to know which capacity to use, so we can't pass it from here.
capacity = component.get_capacity(None, raw=True)
interaction = component.get_interaction()
capacity = interaction.get_capacity(None, None, None, None, raw=True)
values = capacity.get_values()
#cap_name = self.namingTemplates['variable'].format(unit=name, feature='capacity')
if isinstance(values, (list, float)):
Expand Down
Loading

0 comments on commit 2e84471

Please sign in to comment.