Skip to content

Commit

Permalink
Refactor Pyomo Dispatch (#326)
Browse files Browse the repository at this point in the history
* Refactor Pyomo Dispatch

* Refactor dispatch method to be more clear

* More refactoring of pyomo_dispatch

* Testing out PyomoModelHandler

* Fix PyomoModelHandler

* Working PMH for now...

* Remove Commented Code and Refactor Dispatcher

* Add Docstrings

* Get Tests Passing

* Trim trailing whitespace

* Address Comments + Other Minor Changes

* Fix more docstrings

* Trim trailing whitespace

* Fix Docstrings
  • Loading branch information
dylanjm authored Jan 24, 2024
1 parent 48cb977 commit 8495d93
Show file tree
Hide file tree
Showing 7 changed files with 1,290 additions and 1,217 deletions.
11 changes: 0 additions & 11 deletions src/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,17 +92,6 @@ def get_farm_loc(raven_path=None): # Added by Haoyu Wang, May 25, 2022
farm_loc = plugin_handler.getPluginLocation('FARM')
return farm_loc

def get_all_resources(components):
"""
Provides a set of all resources used among all components
@ In, components, list, HERON component objects
@ Out, resources, list, resources used in case
"""
res = set()
for comp in components:
res.update(comp.get_resources())
return res

def get_project_lifetime(case, components):
"""
obtains the project lifetime
Expand Down
43 changes: 43 additions & 0 deletions src/dispatch/DispatchState.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,3 +199,46 @@ def set_activity_vector(self, comp, res, values, tracker='production', start_idx

r = self._resources[comp][res]
self._data[f'{comp.name}_{tracker}'][r, start_idx:end_idx] = values

# DispatchState for Pyomo dispatcher
class PyomoState(DispatchState):
def __init__(self):
"""
Constructor.
@ In, None
@ Out, None
"""
DispatchState.__init__(self)
self._model = None # Pyomo model object

def initialize(self, components, resources_map, times, model):
"""
Connect information about this State to other objects
@ In, components, list, HERON components
@ In, resources_map, dict, map of component names to resources used
@ In, times, np.array, values of "time" this state represents
@ In, model, pyomo.Model, associated model for this state
@ Out, None
"""
DispatchState.initialize(self, components, resources_map, times)
self._model = model

def get_activity_indexed(self, comp, activity, r, t, valued=True, **kwargs):
"""
Getter for activity level.
@ In, comp, HERON Component, component whose information should be retrieved
@ In, activity, str, tracking variable name for activity subset
@ In, r, int, index of resource to retrieve (as given by meta[HERON][resource_indexer])
@ In, t, int, index of time at which activity should be provided
@ In, valued, bool, optional, if True then get float value instead of pyomo expression
@ In, kwargs, dict, additional pass-through keyword arguments
@ Out, activity, float, amount of resource "res" produced/consumed by "comp" at time "time";
note positive is producting, negative is consuming
"""
prod = getattr(self._model, f'{comp.name}_{activity}')[r, t]
if valued:
return prod()
return prod

def set_activity_indexed(self, comp, r, t, value, valued=False):
raise NotImplementedError
114 changes: 8 additions & 106 deletions src/dispatch/Dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,16 @@
"""
Base class for dispatchers.
"""
from ravenframework.utils import InputData, InputTypes
from ravenframework.utils import InputData
from ravenframework.BaseClasses import MessageUser, InputDataUser

class DispatchError(Exception):
"""
Custom exception for dispatch errors.
"""
pass


class Dispatcher(MessageUser, InputDataUser):
"""
Base class for strategies for consecutive dispatching in a continuous period.
Expand Down Expand Up @@ -122,108 +129,3 @@ def validate(self, components, activity, times, meta):
# no validator, nothing needs to be changed
return {}

# ---------------------------------------------
# UTILITY METHODS
def _compute_cashflows(self, components, activity, times, meta, state_args=None, time_offset=0):
"""
Method to compute CashFlow evaluations given components and their activity.
@ In, components, list, HERON components whose cashflows should be evaluated
@ In, activity, DispatchState instance, activity by component/resources/time
@ In, times, np.array(float), time values to evaluate; may be length 1 or longer
@ In, meta, dict, additional info to be passed through to functional evaluations
@ In, state_args, dict, optional, additional arguments to pass while getting activity state
@ In, time_offset, int, optional, increase time index tracker by this value if provided
@ Out, total, float, total cashflows for given components
"""
if state_args is None:
state_args = {}

if meta['HERON']['Case'].use_levelized_inner:
total = self._compute_levelized_cashflows(components, activity, times, meta, state_args, time_offset)
return total

total = 0
specific_meta = dict(meta) # TODO what level of copying do we need here?
resource_indexer = meta['HERON']['resource_indexer']

#print('DEBUGG computing cashflows!')
for comp in components:
#print(f'DEBUGG ... comp {comp.name}')
specific_meta['HERON']['component'] = comp
comp_subtotal = 0
for t, time in enumerate(times):
#print(f'DEBUGG ... ... time {t}')
# NOTE care here to assure that pyomo-indexed variables work here too
specific_activity = {}
for tracker in comp.get_tracking_vars():
specific_activity[tracker] = {}
for resource in resource_indexer[comp]:
specific_activity[tracker][resource] = activity.get_activity(comp, tracker, resource, time, **state_args)
specific_meta['HERON']['time_index'] = t + time_offset
specific_meta['HERON']['time_value'] = time
cfs = comp.get_state_cost(specific_activity, specific_meta, marginal=True)
time_subtotal = sum(cfs.values())
comp_subtotal += time_subtotal
total += comp_subtotal
return total

def _compute_levelized_cashflows(self, components, activity, times, meta, state_args=None, time_offset=0):
"""
Method to compute CashFlow evaluations given components and their activity.
@ In, components, list, HERON components whose cashflows should be evaluated
@ In, activity, DispatchState instance, activity by component/resources/time
@ In, times, np.array(float), time values to evaluate; may be length 1 or longer
@ In, meta, dict, additional info to be passed through to functional evaluations
@ In, state_args, dict, optional, additional arguments to pass while getting activity state
@ In, time_offset, int, optional, increase time index tracker by this value if provided
@ Out, total, float, total cashflows for given components
"""
total = 0
specific_meta = dict(meta) # TODO what level of copying do we need here?
resource_indexer = meta['HERON']['resource_indexer']

# How does this work?
# The general equation looks like:
#
# SUM(Non-Multiplied Terms) + x * SUM(Multiplied Terms) = Target
#
# and we are solving for `x`. Target is 0 by default. Terms here are marginal cashflows.
# Summations here occur over: components, time steps, tracking variables, and resources.
# Typically, there is only 1 multiplied term/cash flow.

multiplied = 0
non_multiplied = 0

for comp in components:
specific_meta['HERON']['component'] = comp
multiplied_comp = 0
non_multiplied_comp = 0
for t, time in enumerate(times):
# NOTE care here to assure that pyomo-indexed variables work here too
specific_activity = {}
for tracker in comp.get_tracking_vars():
specific_activity[tracker] = {}
for resource in resource_indexer[comp]:
specific_activity[tracker][resource] = activity.get_activity(comp, tracker, resource, time, **state_args)
specific_meta['HERON']['time_index'] = t + time_offset
specific_meta['HERON']['time_value'] = time
cfs = comp.get_state_cost(specific_activity, specific_meta, marginal=True)

# there is an assumption here that if a component has a levelized cost, marginal cashflow
# then it is the only marginal cashflow
if comp.levelized_meta:
for cf in comp.levelized_meta.keys():
lcf = cfs.pop(cf) # this should be ok as long as HERON init checks are successful
multiplied_comp += lcf
else:
time_subtotal = sum(cfs.values())
non_multiplied_comp += time_subtotal

multiplied += multiplied_comp
non_multiplied += non_multiplied_comp

# at this point, there should be a not None NPV Target
multiplied += self._eps
total = (meta['HERON']['Case'].npv_target - non_multiplied) / multiplied
total *= -1
return total
Loading

0 comments on commit 8495d93

Please sign in to comment.