Repository: idaholab/HERON (language: python)

Issue:
[TASK] Round-Trip Efficiencty
--------
Issue Description
--------

A characterizing feature of storage technologies is the "round-trip efficiency" (RTE) which represents a lossy factor when energy is stored and released. We should add this.

----------------
For Change Control Board: Issue Review
----------------
This review should occur before any development is performed as a response to this issue.
- [x] 1. Is it tagged with a type: defect or task?
- [x] 2. Is it tagged with a priority: critical, normal or minor?
- [x] 3. If it will impact requirements or requirements tests, is it tagged with requirements?
- [x] 4. If it is a defect, can it cause wrong results for users? If so an email needs to be sent to the users.
- [x] 5. Is a rationale provided? (Such as explaining why the improvement is needed or why current code is wrong.)

-------
For Change Control Board: Issue Closure
-------
This review should occur when the issue is imminently going to be closed.
- [x] 1. If the issue is a defect, is the defect fixed?
- [x] 2. If the issue is a defect, is the defect tested for in the regression test system?  (If not explain why not.)
- [x] 3. If the issue can impact users, has an email to the users group been written (the email should specify if the defect impacts stable or master)?
- [x] 4. If the issue is a defect, does it impact the latest release branch? If yes, is there any issue tagged with release (create if needed)?
- [x] 5. If the issue is being closed without a pull request, has an explanation of why it is being closed been provided?



Discussion / hints:

Fix patch:
diff --git a/src/Components.py b/src/Components.py
index 83567c11..58699b51 100644
--- a/src/Components.py
+++ b/src/Components.py
@@ -166,6 +166,15 @@ def get_interaction(self):
     except IndexError: # there are no interactions!
       return None
 
+  def get_sqrt_RTE(self):
+    """
+      Provide the square root of the round-trip efficiency for this component.
+      Note we use the square root due to splitting loss across the input and output.
+      @ In, None
+      @ Out, RTE, float, round-trip efficiency as a multiplier
+    """
+    return self.get_interaction().get_sqrt_RTE()
+
   def print_me(self, tabs=0, tab='  '):
     """
       Prints info about self
@@ -236,6 +245,14 @@ def get_capacity_var(self):
     """
     return self.get_interaction().get_capacity_var()
 
+  def get_tracking_vars(self):
+    """
+      Provides the variables used by this component to track dispatch
+      @ In, None
+      @ Out, get_tracking_vars, list, variable name list
+    """
+    return self.get_interaction().get_tracking_vars()
+
   def is_dispatchable(self):
     """
       Returns the dispatchability indicator of this component.
@@ -384,6 +401,7 @@ def get_input_specs(cls):
         descr=r"""indicates the resource that defines the minimum activity level for this component,
               as with the component's capacity.""")
     specs.addSub(minn)
+
     return specs
 
   def __init__(self, **kwargs):
@@ -403,6 +421,8 @@ def __init__(self, **kwargs):
     self._function_method_map = {}      # maps things that call functions to the method within the function that needs calling
     self._transfer = None               # the production rate (if any), in produces per consumes
                                         #   for example, {(Producer, 'capacity'): 'method'}
+    self._sqrt_rte = 1.0                # sqrt of the round-trip efficiency for this interaction
+    self._tracking_vars = []            # list of trackable variables for dispatch activity
 
   def read_input(self, specs, mode, comp_name):
     """
@@ -503,6 +523,15 @@ def get_minimum(self, meta, raw=False):
       evaluated, meta = self._minimum.evaluate(meta, target_var=self._minimum_var)
     return evaluated, meta
 
+  def get_sqrt_RTE(self):
+    """
+      Provide the square root of the round-trip efficiency for this component.
+      Note we use the square root due to splitting loss across the input and output.
+      @ In, None
+      @ Out, RTE, float, round-trip efficiency as a multiplier
+    """
+    return self._sqrt_rte
+
   def get_crossrefs(self):
     """
       Getter.
@@ -549,6 +578,14 @@ def get_resources(self):
     """
     return list(self.get_inputs()) + list(self.get_outputs())
 
+  def get_tracking_vars(self):
+    """
+      Provides the variables used by this component to track dispatch
+      @ In, None
+      @ Out, get_tracking_vars, list, variable name list
+    """
+    return self._tracking_vars
+
   def is_dispatchable(self):
     """
       Getter. Indicates if this interaction is Fixed, Dependent, or Independent.
@@ -682,6 +719,7 @@ def __init__(self, **kwargs):
     Interaction.__init__(self, **kwargs)
     self._produces = []   # the resource(s) produced by this interaction
     self._consumes = []   # the resource(s) consumed by this interaction
+    self._tracking_vars = ['production']
 
   def read_input(self, specs, mode, comp_name):
     """
@@ -860,6 +898,8 @@ def get_input_specs(cls):
     specs.addSub(vp_factory.make_input_specs('initial_stored', descr=descr))
     descr=r"""control strategy for operating the storage. If not specified, uses a perfect foresight strategy. """
     specs.addSub(vp_factory.make_input_specs('strategy', allowed=['Function'], descr=descr))
+    descr = r"""round-trip efficiency for this component as a scalar multiplier. \default{1.0}"""
+    specs.addSub(InputData.parameterInputFactory('RTE', contentType=InputTypes.FloatType, descr=descr))
     return specs
 
   def __init__(self, **kwargs):
@@ -873,6 +913,7 @@ def __init__(self, **kwargs):
     self._rate = None           # the rate at which this component can store up or discharge
     self._initial_stored = None # how much resource does this component start with stored?
     self._strategy = None       # how to operate storage unit
+    self._tracking_vars = ['level', 'charge', 'discharge'] # stored quantity, charge activity, discharge activity
 
   def read_input(self, specs, mode, comp_name):
     """
@@ -892,6 +933,8 @@ def read_input(self, specs, mode, comp_name):
         self._set_valued_param('_initial_stored', comp_name, item, mode)
       elif item.getName() == 'strategy':
         self._set_valued_param('_strategy', comp_name, item, mode)
+      elif item.getName() == 'RTE':
+        self._sqrt_rte = np.sqrt(item.value)
     assert len(self._stores) == 1, 'Multiple storage resources given for component "{}"'.format(comp_name)
     self._stores = self._stores[0]
     # checks and defaults
@@ -1086,6 +1129,7 @@ def __init__(self, **kwargs):
     Interaction.__init__(self, **kwargs)
     self._demands = None  # the resource demanded by this interaction
     self._penalty = None  # how to penalize for not meeting demand NOT IMPLEMENTED
+    self._tracking_vars = ['production']
 
   def read_input(self, specs, mode, comp_name):
     """
diff --git a/src/DispatchManager.py b/src/DispatchManager.py
index cc60c037..68c09f8c 100644
--- a/src/DispatchManager.py
+++ b/src/DispatchManager.py
@@ -38,7 +38,7 @@ class DispatchRunner:
   """
   # TODO move naming templates to a common place for consistency!
   naming_template = {'comp capacity': '{comp}_capacity',
-                     'dispatch var': 'Dispatch__{comp}__{res}',
+                     'dispatch var': 'Dispatch__{comp}__{tracker}__{res}',
                     }
 
   def __init__(self):
@@ -220,16 +220,7 @@ def save_variables(self, raven, all_dispatch, metrics):
       @ In, all_dispatch, dict, dispatch values
       @ In, metrics, dict, economic metrics
     """
-    # TODO if debug mode...
-    #   indexer = dict((comp, dict((res, r) for r, res in enumerate(comp.get_resources()))) for comp in self._components)
-    #   shape = [len(dispatch), len(next(iter(dispatch)))] # years, clusters
     template = self.naming_template['dispatch var']
-    # initialize variables TODO if debug mode ...
-    # for comp in self._components:
-    #   for res, r in indexer[comp].items():
-    #     name = template.format(c=comp.name, r=res)
-    #     setattr(raven, name, np.zeros(shape)) # FIXME need time!
-    ##### FIXME year_data is now empty, so none of the following gets run!
     for y, (year, year_data) in enumerate(all_dispatch.items()):
       for c, (cluster, dispatch) in enumerate(year_data.items()):
         dispatches = dispatch.create_raven_vars(template)
@@ -256,19 +247,6 @@ def save_variables(self, raven, all_dispatch, metrics):
             setattr(raven, var_name, np.empty(shape)) # NOTE could use np.zeros, but slower?
           getattr(raven, var_name)[y, c] = data
           getattr(raven, '_indexMap')[0][var_name] = [year_name, clst_name, time_name]
-        #for component in self._components:
-          # TODO cheating using the numpy state
-        #  dispatch = cluster_data['dispatch']
-        #  resource_indices = cluster_data._resources[component]
-    # TODO clustering, multiyear
-    # TODO should this be a Runner method or separate?
-    # TODO if debug mode ...
-    # template = self.naming_template['dispatch var']
-    # for comp_name, data in dispatch.items():
-    #   for resource, usage in data.items():
-    #     name = template.format(comp=comp_name, res=resource)
-    #     setattr(raven, name, usage)
-    #     # TODO indexMap?
     for metric, value in metrics.items():
       setattr(raven, metric, np.atleast_1d(value))
     # if component capacities weren't given by Outer, save them as part of Inner
@@ -492,8 +470,10 @@ def _segment_cashflow(self, meta, s, seg, year, dispatch, multiplicity,
           elif heron_cf.get_period() == 'hour':
             for t, time in enumerate(times):
               # fill in the specific activity for this time stamp
-              for resource, r in resource_indexer[comp].items():
-                specific_activity[resource] = dispatch.get_activity(comp, resource, time)
+              for track_var in comp.get_tracking_vars():
+                specific_activity[track_var] = {}
+                for resource, r in resource_indexer[comp].items():
+                  specific_activity[track_var][resource] = dispatch.get_activity(comp, track_var, resource, time)
               specific_meta['HERON']['time_index'] = t
               specific_meta['HERON']['time_value'] = time
               # TODO does the rest need to be available?
diff --git a/src/DispatchPlot.py b/src/DispatchPlot.py
index 431447fb..adba512b 100644
--- a/src/DispatchPlot.py
+++ b/src/DispatchPlot.py
@@ -95,7 +95,11 @@ def run(self):
       @ In, None
       @ Out, None
     """
-    df = self._source.asDataset().to_dataframe().reset_index()
+    ds = self._source.asDataset()
+    if ds is None:
+      self.raiseAWarning(f'No data in "{self._source.name}" data object; nothing to plot!')
+      return
+    df = ds.to_dataframe().reset_index()
     dispatch_vars = list(filter(lambda x: "Dispatch__" in x, df.columns))
     grouped_vars = self._group_by(dispatch_vars)
 
@@ -111,8 +115,18 @@ def run(self):
             ax = fig.add_subplot(len(grouped_vars),1,i+1)
             for var in group:
               # Plot the micro-step variable on the x-axis (i.ee Time)
-              var_label = var.split('__')[1].replace('_', ' ').title()
-              ax.plot(dat.iloc[:, 1], dat[var], label=var_label)
+              _, comp_name, tracker, resource = var.split('__')
+              comp_label = comp_name.replace('_', ' ').title()
+              # NOTE custom behavior based on production/storage labels
+              if tracker == 'production':
+                var_label = comp_label
+              else:
+                var_label = f'{comp_label}, {tracker.title()}'
+              if tracker == 'level':
+                style = '.:'
+              else:
+                style = '.-'
+              ax.plot(dat.iloc[:, 1], dat[var], style, label=var_label)
               ax.set_title(key.title())
               ax.set_xlabel('Time')
               ax.legend(loc='center left', bbox_to_anchor=(1, 0.5))
diff --git a/src/Economics.py b/src/Economics.py
index f9c7ad22..2b8296ae 100644
--- a/src/Economics.py
+++ b/src/Economics.py
@@ -78,14 +78,15 @@ def set_crossrefs(self, refs):
     """
     self._economics.set_crossrefs(refs)
 
-  def get_state_cost(self, activity, meta):
+  def get_state_cost(self, activity, meta, marginal=False):
     """
       get the cost given particular activities (state) of the cash flow user
       @ In, raven_vars, dict, additional variables (presumably from raven) that might be needed
       @ In, meta, dict, further dictionary of information that might be needed
+      @ In, marginal, bool, optional, if True then only get marginal costs
       @ Out, cost, dict, cost of activity as a breakdown
     """
-    return self.get_economics().evaluate_cfs(activity, meta)
+    return self.get_economics().evaluate_cfs(activity, meta, marginal=marginal)
 
   def get_economics(self):
     """
@@ -184,15 +185,24 @@ def set_crossrefs(self, refs):
   #######
   # API #
   #######
-  def evaluate_cfs(self, activity, meta):
+  def evaluate_cfs(self, activity, meta, marginal=False):
     """
       Calculates the incremental cost of a particular system configuration.
       @ In, activity, XArray.DataArray, array of driver-centric variable values
       @ In, meta, dict, additional user-defined meta
+      @ In, marginal, bool, optional, if True then only get marginal cashflows (e.g. recurring hourly)
       @ Out, cost, dict, cash flow evaluations
     """
     # combine all cash flows into single cash flow evaluation
-    cost = dict((cf.name, cf.evaluate_cost(activity, meta)) for cf in self.get_cashflows())
+    if marginal:
+      # FIXME assuming 'year' is the only non-marginal value
+      # FIXME why is it "repeating" and not "Recurring"?
+      cost = dict((cf.name, cf.evaluate_cost(activity, meta))
+                    for cf in self.get_cashflows()
+                    if (cf._type == 'repeating' and cf.get_period() != 'year'))
+    else:
+      cost = dict((cf.name, cf.evaluate_cost(activity, meta))
+                    for cf in self.get_cashflows())
     return cost
 
   def get_cashflows(self):
diff --git a/src/ValuedParams/Activity.py b/src/ValuedParams/Activity.py
index fd7445b9..aeb84fa1 100644
--- a/src/ValuedParams/Activity.py
+++ b/src/ValuedParams/Activity.py
@@ -26,6 +26,10 @@ def get_input_specs(cls):
               The value of this node should be the name of a resource that this component
               either produces or consumes. Note the sign of the activity by default will be negative
               for consumed resources and positive for produced resources.""")
+    spec.addParam('tracking', param_type=InputTypes.StringType, required=False,
+        descr=r"""Some kinds of components have multiple tracking variables. This attribute specifies
+               which tracking variable is desired. Options are only "production" for Production and Demand
+               units, and [level, charge, discharge] for Storage units. Default is the first entry listed.""")
     return spec
 
   def __init__(self):
@@ -38,6 +42,7 @@ def __init__(self):
     self._source_kind = 'activity'
     self._var_name = None # name of the variable within the synth hist
     self._resource = None # name of the resource whose activity should be used
+    self._tracking_var = None # specific tracking variable for the component
 
   def read(self, comp_name, spec, mode, alias_dict=None):
     """
@@ -49,6 +54,8 @@ def read(self, comp_name, spec, mode, alias_dict=None):
       @ Out, needs, list, signals needed to evaluate this ValuedParam at runtime
     """
     super().read(comp_name, spec, mode, alias_dict=None)
+    subvar = spec.parameterValues.get('tracking', None)
+    self._tracking_var = subvar # NOTE this gets fixed up in the crosscheck
     # aliases get used to convert variable names, notably for the cashflow's "capacity"
     if alias_dict is None:
       alias_dict = {}
@@ -62,6 +69,15 @@ def crosscheck(self, interaction):
       @ In, interaction, HERON.Component.Interaction, interaction that "owns" this VP
       @ Out, None
     """
+    # use this chance to link the interaction tracking vars
+    ok_trackers = interaction.get_tracking_vars()
+    if self._tracking_var is None:
+      self._tracking_var = ok_trackers[0]
+      self.raiseAMessage(f'Tracking variable not specified; using "{self._tracking_var}" ...')
+    else:
+      if self._tracking_var not in ok_trackers:
+        self.raiseAnError(f'Tracking variable "{self._tracking_var}" is not one of the variables ' +
+                           f'tracked by this interaction! Options are: {ok_trackers}.')
     # check that the requested resource is actually used by this interaction
     available = interaction.get_resources()
     if self._resource not in available:
@@ -84,7 +100,7 @@ def evaluate(self, inputs, target_var=None, aliases=None):
     # set the outgoing name for the evaluation results
     key = self._var_name if not target_var else target_var
     try:
-      value = inputs['HERON']['activity'][self._resource]
+      value = inputs['HERON']['activity'][self._tracking_var][self._resource]
     except KeyError as e:
       self.raiseAnError(RuntimeError, f'Resource "{self._resource}" was not found among those produced and ' +
                         'consumed by this component!')
diff --git a/src/dispatch/DispatchState.py b/src/dispatch/DispatchState.py
index e74e5cf4..30b5ba60 100644
--- a/src/dispatch/DispatchState.py
+++ b/src/dispatch/DispatchState.py
@@ -36,10 +36,11 @@ def __repr__(self):
     """
     return '<HERON generic DispatchState object>'
 
-  def get_activity(self, comp, res, time, **kwargs):
+  def get_activity(self, comp, activity, res, time, **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, res, string, name of resource to retrieve
       @ In, time, float, time at which activity should be provided
       @ Out, activity, float, amount of resource "res" produced/consumed by "comp" at time "time";
@@ -47,12 +48,13 @@ def get_activity(self, comp, res, time, **kwargs):
     """
     r = self._resources[comp][res]
     t = np.searchsorted(self._times, time) # TODO protect against value not present
-    return self.get_activity_indexed(comp, r, t, **kwargs)
+    return self.get_activity_indexed(comp, activity, r, t, **kwargs)
 
-  def set_activity(self, comp, res, time, value, **kwargs):
+  def set_activity(self, comp, activity, res, time, value, **kwargs):
     """
       Setter for activity level.
       @ In, comp, HERON Component, component whose information should be set
+      @ In, activity, str, tracking variable name for activity subset
       @ In, res, string, name of resource to retrieve
       @ In, time, float, time at which activity should be provided
       @ In, value, float, activity level; note positive is producting, negative is consuming
@@ -61,9 +63,9 @@ def set_activity(self, comp, res, time, value, **kwargs):
     """
     r = self._resources[comp][res]
     t = np.searchsorted(self._times, time) # TODO protect against value not present
-    self.set_activity_indexed(comp, r, t, value, **kwargs)
+    self.set_activity_indexed(comp, activity, r, t, value, **kwargs)
 
-  def get_activity_indexed(self, comp, r, t, **kwargs):
+  def get_activity_indexed(self, *args, **kwargs):
     """
       Getter for activity level, using indexes instead of values for r and t
       @ In, comp, HERON Component, component whose information should be retrieved
@@ -76,13 +78,10 @@ def get_activity_indexed(self, comp, r, t, **kwargs):
     # to be overwritten by implementing classes
     raise NotImplementedError
 
-  def set_activity_indexed(self, comp, r, t, value, **kwargs):
+  def set_activity_indexed(self, *args, **kwargs):
     """
       Getter for activity level, using indexes instead of values for r and t
-      @ In, comp, HERON Component, component whose information should be retrieved
-      @ 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, value, float, value to set for activity
+      @ In, args, list, additional pass-through arguments
       @ 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
@@ -99,11 +98,12 @@ def create_raven_vars(self, template):
     #template = 'Dispatch__{c}__{r}' # standardized via input
     data = {}
     for comp in self._components:
-      for res, r in self._resources[comp].items():
-        result = np.empty(len(self._times))
-        for t, time in enumerate(self._times):
-          result[t] = self.get_activity_indexed(comp, r, t)
-        data[template.format(comp=comp.name, res=res)] = result
+      for tracker in comp.get_tracking_vars():
+        for res, r in self._resources[comp].items():
+          result = np.empty(len(self._times))
+          for t, time in enumerate(self._times):
+            result[t] = self.get_activity_indexed(comp, tracker, r, t)
+          data[template.format(comp=comp.name, tracker=tracker, res=res)] = result
     return data
 
 # NumpyState is the nominal DispatchState implementation
@@ -129,7 +129,8 @@ def initialize(self, components, resources_map, times):
     DispatchState.initialize(self, components, resources_map, times)
     self._data = {}
     for comp in components:
-      self._data[comp] = np.zeros((len(self._resources[comp]), len(times)))
+      for tag in comp.get_tracking_vars():
+        self._data[f'{comp.name}_{tag}'] = np.zeros((len(self._resources[comp]), len(times)))
 
   def __repr__(self):
     """
@@ -139,43 +140,51 @@ def __repr__(self):
     """
     msg = StringIO()
     msg.write('<HERON NumpyState dispatch record: \n')
-    for comp in self._data:
+    for key, act_data in self._data.items():
+      name, activity = key.split('_')
+      # find corresponding component
+      for comp, resources in self._resources.items():
+        if comp.name == name:
+          break
       resources = self._resources[comp]
-      msg.write(f'   component: {comp.name}\n')
+      msg.write(f'   component: {name} activity: {activity}\n')
       for res, r in resources.items():
-        msg.write(f'      {res}: {self._data[comp][r]}\n')
+        msg.write(f'      {res}: {act_data[r]}\n')
     msg.write('END NumpyState dispatch record>')
     return msg.getvalue()
 
-  def get_activity_indexed(self, comp, r, t, **kwargs):
+  def get_activity_indexed(self, comp, activity, r, t, **kwargs):
     """
       Getter for activity level.
       Note, if any of the arguments are "None" it is assumed that means "all"
       @ 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, 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
     """
-    return self._data[comp][r, t]
+    return self._data[f'{comp.name}_{activity}'][r, t]
 
-  def set_activity_indexed(self, comp, r, t, value, **kwargs):
+  def set_activity_indexed(self, comp, activity, r, t, value, **kwargs):
     """
       Setter for activity level.
       @ In, comp, HERON Component, component whose information should be set
+      @ In, activity, str, tracking variable name for activity subset
       @ In, res, string, name of resource to retrieve
       @ In, time, float, time at which activity should be provided
       @ In, value, float, activity level; note positive is producting, negative is consuming
       @ In, kwargs, dict, additional pass-through keyword arguments
       @ Out, None
     """
-    self._data[comp][r, t] = value
+    self._data[f'{comp.name}_{activity}'][r, t] = value
 
-  def set_activity_vector(self, comp, res, start_time, end_time, values):
+  def set_activity_vector(self, comp, activity, res, start_time, end_time, values):
     """
       Shortcut utility for setting values all-at-once in a vector.
       @ In, comp, HERON Component, component whose information should be set
+      @ In, activity, str, tracking variable name for activity subset
       @ In, res, string, name of resource to retrieve
       @ In, start_time, int, first time index at which activity is provided
       @ In, end_time, int, last time at which activity is provided (not inclusive)
@@ -183,4 +192,4 @@ def set_activity_vector(self, comp, res, start_time, end_time, values):
       @ Out, None
     """
     r = self._resources[comp][res]
-    self._data[comp][r, start_time:end_time] = values
+    self._data[f'{comp.name}_{activity}'][r, start_time:end_time] = values
diff --git a/src/dispatch/Dispatcher.py b/src/dispatch/Dispatcher.py
index 2ee551ab..ab3dc673 100644
--- a/src/dispatch/Dispatcher.py
+++ b/src/dispatch/Dispatcher.py
@@ -139,11 +139,13 @@ def _compute_cashflows(self, components, activity, times, meta, state_args=None,
         #print(f'DEBUGG ... ... time {t}')
         # NOTE care here to assure that pyomo-indexed variables work here too
         specific_activity = {}
-        for resource in resource_indexer[comp]:
-          specific_activity[resource] = activity.get_activity(comp, resource, time, **state_args)
+        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)
+        cfs = comp.get_state_cost(specific_activity, specific_meta, marginal=True)
         time_subtotal = sum(cfs.values())
         comp_subtotal += time_subtotal
       total += comp_subtotal
diff --git a/src/dispatch/pyomo_dispatch.py b/src/dispatch/pyomo_dispatch.py
index a2aa80f7..b44c8035 100644
--- a/src/dispatch/pyomo_dispatch.py
+++ b/src/dispatch/pyomo_dispatch.py
@@ -11,6 +11,7 @@
 from functools import partial
 import platform
 from itertools import compress
+import pprint
 
 import numpy as np
 import pyomo.environ as pyo
@@ -180,7 +181,7 @@ def dispatch(self, case, components, sources, meta):
           if start_index == 0:
             initial_levels[comp] = comp.get_interaction().get_initial_level(meta)
           else:
-            initial_levels[comp] = subdisp[comp.name][comp.get_interaction().get_resource()][-1]
+            initial_levels[comp] = subdisp[comp.name]['level'][comp.get_interaction().get_resource()][-1]
       # allow for converging solution iteratively
       converged = False
       conv_counter = 0
@@ -205,8 +206,9 @@ def dispatch(self, case, components, sources, meta):
       print('DEBUGG solve time: {} s'.format(end-start))
       # store result in corresponding part of dispatch
       for comp in components:
-        for res, values in subdisp[comp.name].items():
-          dispatch.set_activity_vector(comp, res, start_index, end_index, values)
+        for tag in comp.get_tracking_vars():
+          for res, values in subdisp[comp.name][tag].items():
+            dispatch.set_activity_vector(comp, tag, res, start_index, end_index, values)
       start_index = end_index
     return dispatch
 
@@ -238,6 +240,7 @@ def dispatch_window(self, time, time_offset,
     m.R = pyo.Set(initialize=R)
     m.T = pyo.Set(initialize=T)
     m.Times = time
+    dt = m.Times[1] - m.Times[0] # TODO assumes consistent step sizing
     m.time_offset = time_offset
     m.resource_index_map = meta['HERON']['resource_indexer'] # maps the resource to its index WITHIN APPLICABLE components (sparse matrix)
                                                              #   e.g. component: {resource: local index}, ... etc}
@@ -254,17 +257,40 @@ def dispatch_window(self, time, time_offset,
       intr = comp.get_interaction()
       if intr.is_governed():
         meta['request'] = {'component': comp, 'time': time}
-        activity = intr.get_strategy().evaluate(meta)[0]['activity']
-        self._create_production_param(m, comp, activity)
+        activity = intr.get_strategy().evaluate(meta)[0]['level']
+        if intr.is_type('Storage'):
+          self._create_production_param(m, comp, activity, tag='level')
+          # set up "activity" rates (change in level over time, plus efficiency)
+          rte2 = comp.get_sqrt_RTE() # square root of the round-trip efficiency
+          L  = len(activity)
+          deltas = np.zeros(L)
+          deltas[1:] = activity[1:] - activity[:-1]
+          deltas[0] = activity[0] - intr.get_initial_level(meta)
+          # rate of charge
+          # change sign, since increasing level means absorbing energy from system
+          # also scale by RTE, since to get level increase you have to over-absorb
+          charge = np.zeros(L)
+          charge_mask = np.where(deltas > 0)
+          charge[charge_mask] = - deltas[charge_mask] / dt / rte2
+          self._create_production_param(m, comp, charge, tag='charge')
+          # rate of discharge
+          # change sign, since decreasing level means emitting energy into system
+          # also scale by RTE, since level decrease yields less to system
+          discharge = np.zeros(L)
+          discharge_mask = np.where(deltas < 0)
+          discharge[discharge_mask] = - deltas[discharge_mask] / dt * rte2
+          self._create_production_param(m, comp, discharge, tag='discharge')
+        else:
+          self._create_production_param(m, comp, activity)
         continue
       # NOTE: "fixed" components could hypothetically be treated differently
       ## however, in order for the "production" variable for components to be treatable as the
       ## 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, meta)    # capacity constraints
-      self._create_transfer(m, comp, prod_name)    # transfer functions (constraints)
-      # ramp rates TODO ## INCLUDING previous-time boundary condition TODO
+      if intr.is_type('Storage'):
+        self._create_storage(m, comp, initial_storage, meta)
+      else:
+        self._create_production(m, comp, meta) # variables
     self._create_conservation(m, resources, initial_storage, meta) # conservation of resources (e.g. production == consumption)
     self._create_objective(meta, m) # objective
     # start a solution search
@@ -286,6 +312,8 @@ def dispatch_window(self, time, time_offset,
         print('DEBUGG ... status:', soln.solver.status)
         print('DEBUGG ... termination:', soln.solver.termination_condition)
         self._debug_pyomo_print(m)
+        print('Resource Map:')
+        pprint.pprint(m.resource_index_map)
         raise RuntimeError
       # try validating
       print('DEBUGG ... validating ...')
@@ -330,12 +358,13 @@ def check_converged(self, new, old, components):
     for comp in components:
       intr = comp.get_interaction()
       name = comp.name
+      tracker = comp.get_tracking_vars()[0]
       if intr.is_governed(): # by "is_governed" we mean "isn't optimized in pyomo"
         # check activity L2 norm as a differ
         # TODO this may be specific to storage right now
         res = intr.get_resource()
-        scale = np.max(old[name][res])
-        diff = np.linalg.norm(new[name][res] - old[name][res]) / (scale if scale != 0 else 1)
+        scale = np.max(old[name][tracker][res])
+        diff = np.linalg.norm(new[name][tracker][res] - old[name][tracker][res]) / (scale if scale != 0 else 1)
         if diff > tol:
           converged = False
     return converged
@@ -387,46 +416,94 @@ def _create_production_limit(self, m, validation):
     setattr(m, name, constr)
     print(f'DEBUGG added validation constraint "{name}"')
 
-  def _create_production_param(self, m, comp, values):
+  def _create_production_param(self, m, comp, values, tag=None):
     """
       Creates production pyomo fixed parameter object for a component
       @ In, m, pyo.ConcreteModel, associated model
       @ In, comp, HERON Component, component to make production variables for
+      @ In, values, np.array(float), values to set for param
+      @ In, tag, str, optional, if not None then name will be component_[tag]
       @ Out, prod_name, str, name of production variable
     """
     name = comp.name
+    if tag is None:
+      tag = 'production'
     # create pyomo indexer for this component's resources
     res_indexer = pyo.Set(initialize=range(len(m.resource_index_map[comp])))
     setattr(m, f'{name}_res_index_map', res_indexer)
-    prod_name = '{c}_production'.format(c=name)
+    prod_name = f'{name}_{tag}'
     init = (((0, t), values[t]) for t in m.T)
     prod = pyo.Param(res_indexer, m.T, initialize=dict(init))
     setattr(m, prod_name, prod)
     return prod_name
 
-  def _create_production(self, m, comp):
+  def _create_production(self, m, comp, meta):
+    """
+      Creates all pyomo variable objects for a non-storage component
+      @ In, m, pyo.ConcreteModel, associated model
+      @ In, comp, HERON Component, component to make production variables for
+      @ In, meta, dict, dictionary of state variables
+      @ Out, None
+    """
+    prod_name = self._create_production_variable(m, comp, meta)
+    ## if you cannot set limits directly in the production variable, set separate contraint:
+    ## Method 1: set variable bounds directly --> TODO more work needed, but would be nice
+    # lower, upper = self._get_prod_bounds(m, comp)
+    # limits should be None unless specified, so use "getters" from dictionaries
+    # bounds = lambda m, r, t: (lower.get(r, None), upper.get(r, None))
+    ## Method 2: set variable bounds directly --> TODO more work needed, but would be nice
+    # self._create_capacity(m, comp, prod_name, meta)    # capacity constraints
+    # transfer function governs input -> output relationship
+    self._create_transfer(m, comp, prod_name)
+    # ramp rates TODO ## INCLUDING previous-time boundary condition TODO
+
+  def _create_production_variable(self, m, comp, meta, tag=None, add_bounds=True, **kwargs):
     """
       Creates production pyomo variable object for a component
       @ In, m, pyo.ConcreteModel, associated model
       @ In, comp, HERON Component, component to make production variables for
+      @ In, tag, str, optional, if not None then name will be component_[tag]; otherwise "production"
+      @ In, add_bounds, bool, optional, if True then determine and set bounds for variable
+      @ In, kwargs, dict, optional, passalong kwargs to pyomo variable
       @ Out, prod_name, str, name of production variable
     """
+    if tag is None:
+      tag = 'production'
     name = comp.name
+    cap_res = comp.get_capacity_var()       # name of resource that defines capacity
+    limit_r = m.resource_index_map[comp][cap_res] # production index of the governing resource
     # create pyomo indexer for this component's resources
-    res_indexer = pyo.Set(initialize=range(len(m.resource_index_map[comp])))
-    setattr(m, f'{name}_res_index_map', res_indexer)
-    # production variable depends on resource, time
-    # # TODO if transfer function is linear, isn't this technically redundant? Maybe only need one resource ...
-    ## Method 1: set variable bounds directly --> not working! why??
-    #lower, upper, domain = self._get_prod_bounds(comp)
-    #prod = pyo.Var(res_indexer, m.T, bounds=(lower, upper)) #within=domain,
-    ## Method 2: set capacity as a seperate constraint
-    prod_name = '{c}_production'.format(c=name)
-    prod = pyo.Var(res_indexer, m.T, initialize=0)
+    indexer_name = f'{name}_res_index_map'
+    indexer = getattr(m, indexer_name, None)
+    if indexer is None:
+      indexer = pyo.Set(initialize=range(len(m.resource_index_map[comp])))
+      setattr(m, indexer_name, indexer)
+    prod_name = f'{name}_{tag}'
+    caps, mins = self._find_production_limits(m, comp, meta)
+    if min(caps) < 0:
+      # we have a unit that's consuming, so we need to flip the variables to be sensible
+      mins, caps = caps, mins
+      inits = caps
+    else:
+      inits = mins
+    if add_bounds:
+      # create bounds based in min, max operation
+      bounds = lambda m, r, t: (mins[t] if r == limit_r else None, caps[t] if r == limit_r else None)
+      initial = lambda m, r, t: inits[t] if r == limit_r else 0
+    else:
+      bounds = (None, None)
+      initial = 0
+    # production variable depends on resources, time
+    #FIXME initials! Should be lambda with mins for tracking var!
+    prod = pyo.Var(indexer, m.T, initialize=initial, bounds=bounds, **kwargs)
+    # TODO it may be that we need to set variable values to avoid problems in some solvers.
+    # if comp.is_dispatchable() == 'fixed':
+    #   for t, _ in enumerate(m.Times):
+    #     prod[limit_r, t].fix(caps[t])
     setattr(m, prod_name, prod)
     return prod_name
 
-  def _create_capacity(self, m, comp, prod_name, meta):
+  def _create_capacity_constraints(self, m, comp, prod_name, meta):
     """
       Creates pyomo capacity constraints
       @ In, m, pyo.ConcreteModel, associated model
@@ -435,7 +512,37 @@ def _create_capacity(self, m, comp, prod_name, meta):
       @ In, meta, dict, additional state information
       @ Out, None
     """
-    name = comp.name
+    cap_res = comp.get_capacity_var()       # name of resource that defines capacity
+    r = m.resource_index_map[comp][cap_res] # production index of the governing resource
+    caps, mins = self._find_production_limits(m, comp, meta)
+    # capacity
+    max_rule = partial(self._capacity_rule, prod_name, r, caps)
+    constr = pyo.Constraint(m.T, rule=max_rule)
+    setattr(m, '{c}_{r}_capacity_constr'.format(c=comp.name, r=cap_res), constr)
+    # minimum
+    min_rule = partial(self._min_prod_rule, prod_name, r, caps, mins)
+    constr = pyo.Constraint(m.T, rule=min_rule)
+    # set initial conditions
+    for t, time in enumerate(m.Times):
+      cap = caps[t]
+      if cap == mins[t]:
+        # initialize values so there's no boundary errors
+        var = getattr(m, prod_name)
+        values = var.get_values()
+        for k in values:
+          values[k] = cap
+        var.set_values(values)
+    setattr(m, '{c}_{r}_minprod_constr'.format(c=comp.name, r=cap_res), constr)
+
+  def _find_production_limits(self, m, comp, meta):
+    """
+      Determines the capacity limits of a unit's operation, in time.
+      @ In, m, pyo.ConcreteModel, associated model
+      @ In, comp, HERON Component, component to make variables for
+      @ In, meta, dict, additional state information
+      @ Out, caps, array, max production values by time
+      @ Out, mins, array, min production values by time
+    """
     cap_res = comp.get_capacity_var()       # name of resource that defines capacity
     r = m.resource_index_map[comp][cap_res] # production index of the governing resource
     # production is always lower than capacity
@@ -448,25 +555,12 @@ def _create_capacity(self, m, comp, prod_name, meta):
       meta['HERON']['time_index'] = t + m.time_offset
       cap = comp.get_capacity(meta)[0][cap_res] # value of capacity limit (units of governing resource)
       caps.append(cap)
-      minimum = comp.get_minimum(meta)[0][cap_res]
-      # minimum production
-      if (comp.is_dispatchable() == 'fixed') or (minimum == cap):
+      if (comp.is_dispatchable() == 'fixed'):
         minimum = cap
-        # initialize values so there's no boundary errors
-        var = getattr(m, prod_name)
-        values = var.get_values()
-        for k in values:
-          values[k] = cap
-        var.set_values(values)
+      else:
+        minimum = comp.get_minimum(meta)[0][cap_res]
       mins.append(minimum)
-    # capacity
-    rule = partial(self._capacity_rule, prod_name, r, caps)
-    constr = pyo.Constraint(m.T, rule=rule)
-    setattr(m, '{c}_{r}_capacity_constr'.format(c=name, r=cap_res), constr)
-    # minimum
-    rule = partial(self._min_prod_rule, prod_name, r, caps, mins)
-    constr = pyo.Constraint(m.T, rule=rule)
-    setattr(m, '{c}_{r}_minprod_constr'.format(c=name, r=cap_res), constr)
+    return caps, mins
 
   def _create_transfer(self, m, comp, prod_name):
     """
@@ -492,6 +586,53 @@ def _create_transfer(self, m, comp, prod_name):
       constr = pyo.Constraint(m.T, rule=rule)
       setattr(m, rule_name, constr)
 
+  def _create_storage(self, m, comp, initial_storage, meta):
+    """
+      Creates storage pyomo variable objects for a storage component
+      Similar to create_production, but for storages
+      @ In, m, pyo.ConcreteModel, associated model
+      @ In, comp, HERON Component, component to make production variables for
+      @ In, initial_storage, dict, initial storage levels
+      @ In, meta, dict, additional state information
+      @ Out, level_name, str, name of storage level variable
+    """
+    prefix = comp.name
+    # what resource index? Isn't it always 0? # assumption
+    r = 0 # NOTE this is only true if each storage ONLY uses 1 resource
+    # storages require a few variables:
+    # (1) a level tracker,
+    level_name = self._create_production_variable(m, comp, meta, tag='level')
+    # -> set operational limits
+    # self._create_capacity(m, comp, level_name, meta)
+    # (2, 3) separate charge/discharge trackers, so we can implement round-trip efficiency and ramp rates
+    charge_name = self._create_production_variable(m, comp, meta, tag='charge', add_bounds=False, within=pyo.NonPositiveReals)
+    discharge_name = self._create_production_variable(m, comp, meta, tag='discharge', add_bounds=False, within=pyo.NonNegativeReals)
+    # balance level, charge/discharge
+    level_rule_name = prefix + '_level_constr'
+    rule = partial(self._level_rule, comp, level_name, charge_name, discharge_name,
+                                      initial_storage, r)
+    setattr(m, level_rule_name, pyo.Constraint(m.T, rule=rule))
+    # (4) a binary variable to track whether we're charging or discharging, to prevent BOTH happening
+    # -> 0 is charging, 1 is discharging
+    # -> TODO make this a user-based option to disable, if they want to allow dual operation
+    # -> -> but they should really think about if that's what they want!
+    # FIXME currently introducing the bigM strategy also makes solves numerically unstable,
+    # and frequently results in spurious errors. For now, disable it.
+    allow_both = True # allow simultaneous charging and discharging
+    if not allow_both:
+      bin_name = self._create_production_variable(m, comp, meta, tag='dcforcer', add_bounds=False, within=pyo.Binary)
+      # we need a large epsilon, but not so large that addition stops making sense
+      # -> we don't know what any values for this component will be! How do we choose?
+      # -> NOTE that choosing this value has VAST impact on solve stability!!
+      large_eps = 1e8 #0.01 * sys.float_info.max
+      # charging constraint: don't charge while discharging (note the sign matters)
+      charge_rule_name = prefix + '_charge_constr'
+      rule = partial(self._charge_rule, charge_name, bin_name, large_eps, r)
+      setattr(m, charge_rule_name, pyo.Constraint(m.T, rule=rule))
+      discharge_rule_name = prefix + '_discharge_constr'
+      rule = partial(self._discharge_rule, discharge_name, bin_name, large_eps, r)
+      setattr(m, discharge_rule_name, pyo.Constraint(m.T, rule=rule))
+
   def _create_conservation(self, m, resources, initial_storage, meta):
     """
       Creates pyomo conservation constraints
@@ -518,20 +659,44 @@ def _create_objective(self, meta, m):
     m.obj = pyo.Objective(rule=rule, sense=pyo.maximize)
 
   ### UTILITIES for general use
-  def _get_prod_bounds(self, comp):
+  def _get_prod_bounds(self, m, comp, meta):
     """
       Determines the production limits of the given component
       @ In, comp, HERON component, component to get bounds of
-      @ Out, (min, max, domain), float/float/pyomo domain, limits and domain of variables
+      @ In, meta, dict, additional state information
+      @ Out, lower, dict, lower production limit (might be negative for consumption)
+      @ Out, upper, dict, upper production limit (might be 0 for consumption)
     """
+    raise NotImplementedError
     cap_res = comp.get_capacity_var()       # name of resource that defines capacity
+    r = m.resource_index_map[comp][cap_res]
+    maxs = []
+    mins = []
+    for t, time in enumerate(m.Times):
+      meta['HERON']['time_index'] = t + m.time_offset
+      cap = comp.get_capacity(meta)[0][cap_res]
+      low = comp.get_minimum(meta)[0][cap_res]
+      maxs.append(cap)
+      if (comp.is_dispatchable() == 'fixed') or (low == cap):
+        low = cap
+        # initialize values to avoid boundary errors
+        var = getattr(m, prod_name)
+        values = var.get_values()
+        for k in values:
+          values[k] = cap
+        var.set_values(values)
+      mins.append(low)
     maximum = comp.get_capacity(None, None, None, None)[0][cap_res]
     # TODO minimum!
     # producing or consuming the defining resource?
+    # -> put these in dictionaries so we can "get" limits or return None
     if maximum > 0:
-      return 0, maximum, pyo.NonNegativeReals
+      lower = {r: 0}
+      upper = {r: maximum}
     else:
-      return maximum, 0, pyo.NonPositiveReals
+      lower = {r: maximum}
+      upper = {r: 0}
+    return lower, upper
 
   def _get_transfer_coeffs(self, m, comp):
     """
@@ -570,26 +735,98 @@ def _retrieve_solution(self, m):
     """
       Extracts solution from Pyomo optimization
       @ In, m, pyo.ConcreteModel, associated (solved) model
-      @ Out, result, dict, {comp: {resource: [production], etc}, etc}
+      @ Out, result, dict, {comp: {activity: {resource: [production]}} e.g. generator[production][steam]
     """
-    result = {} # {component: {resource: production}}
+    result = {} # {component: {activity_tag: {resource: production}}}
     for comp in m.Components:
-      prod = getattr(m, '{n}_production'.format(n=comp.name))
-      if isinstance(prod, pyo.Var):
-        kind = 'Var'
-      elif isinstance(prod, pyo.Param):
-        kind = 'Param'
       result[comp.name] = {}
-      for res, comp_r in m.resource_index_map[comp].items():
-        if kind == 'Var':
-          result[comp.name][res] = np.fromiter((prod[comp_r, t].value for t in m.T), dtype=float, count=len(m.T))
-        elif kind == 'Param':
-          result[comp.name][res] = np.fromiter((prod[comp_r, t] for t in m.T), dtype=float, count=len(m.T))
+      for tag in comp.get_tracking_vars():
+        tag_values = self._retrieve_value_from_model(m, comp, tag)
+        result[comp.name][tag] = tag_values
+    return result
+
+  def _retrieve_value_from_model(self, m, comp, tag):
+    """
+      Retrieve values of a series from the pyomo model.
+      @ In, m, pyo.ConcreteModel, associated (solved) model
+      @ In, comp, Component, relevant component
+      @ In, tag, str, particular history type to retrieve
+      @ Out, result, dict, {resource: [array], etc}
+    """
+    result = {}
+    prod = getattr(m, f'{comp.name}_{tag}')
+    if isinstance(prod, pyo.Var):
+      kind = 'Var'
+    elif isinstance(prod, pyo.Param):
+      kind = 'Param'
+    for res, comp_r in m.resource_index_map[comp].items():
+      if kind == 'Var':
+        result[res] = np.fromiter((prod[comp_r, t].value for t in m.T), dtype=float, count=len(m.T))
+      elif kind == 'Param':
+        result[res] = np.fromiter((prod[comp_r, t] for t in m.T), dtype=float, count=len(m.T))
     return result
 
   ### RULES for partial function calls
   # these get called using "functools.partial" to make Pyomo constraints, vars, objectives, etc
 
+  def _charge_rule(self, charge_name, bin_name, large_eps, r, m, t):
+    """
+      Constructs pyomo don't-charge-while-discharging constraints.
+      For storage units specificially.
+      @ In, charge_name, str, name of charging variable
+      @ In, bin_name, str, name of forcing binary variable
+      @ In, large_eps, float, a large-ish number w.r.t. storage activity
+      @ In, r, int, index of stored resource (is this always 0?)
+      @ In, m, pyo.ConcreteModel, associated model
+      @ In, t, int, time index for capacity rule
+      @ Out, rule, bool, inequality used to limit charge behavior
+    """
+    charge_var = getattr(m, charge_name)
+    bin_var = getattr(m, bin_name)
+    return -charge_var[r, t] <= (1 - bin_var[r, t]) * large_eps
+
+  def _discharge_rule(self, discharge_name, bin_name, large_eps, r, m, t):
+    """
+      Constructs pyomo don't-discharge-while-charging constraints.
+      For storage units specificially.
+      @ In, discharge_name, str, name of discharging variable
+      @ In, bin_name, str, name of forcing binary variable
+      @ In, large_eps, float, a large-ish number w.r.t. storage activity
+      @ In, r, int, index of stored resource (is this always 0?)
+      @ In, m, pyo.ConcreteModel, associated model
+      @ In, t, int, time index for capacity rule
+      @ Out, rule, bool, inequality used to limit discharge behavior
+    """
+    discharge_var = getattr(m, discharge_name)
+    bin_var = getattr(m, bin_name)
+    return discharge_var[r, t] <= bin_var[r, t] * large_eps
+
+  def _level_rule(self, comp, level_name, charge_name, discharge_name, initial_storage, r, m, t):
+    """
+      Constructs pyomo charge-discharge-level balance constraints.
+      For storage units specificially.
+      @ In, comp, Component, storage component of interest
+      @ In, level_name, str, name of level-tracking variable
+      @ In, charge_name, str, name of charging variable
+      @ In, discharge_name, str, name of discharging variable
+      @ In, r, int, index of stored resource (is this always 0?)
+      @ In, m, pyo.ConcreteModel, associated model
+      @ In, t, int, time index for capacity rule
+      @ Out, rule, bool, inequality used to limit level behavior
+    """
+    level_var = getattr(m, level_name)
+    charge_var = getattr(m, charge_name)
+    discharge_var = getattr(m, discharge_name)
+    if t > 0:
+      previous = level_var[r, t-1]
+      dt = m.Times[t] - m.Times[t-1]
+    else:
+      previous = initial_storage[comp]
+      dt = m.Times[1] - m.Times[0]
+    rte2 = comp.get_sqrt_RTE() # square root of the round-trip efficiency
+    production = - rte2 * charge_var[r, t] - discharge_var[r, t] / rte2
+    return level_var[r, t] == previous + production * dt
+
   def _capacity_rule(self, prod_name, r, caps, m, t):
     """
       Constructs pyomo capacity constraints.
@@ -648,21 +885,18 @@ def _conservation_rule(self, initial_storage, meta, res, m, t):
     for comp, res_dict in m.resource_index_map.items():
       if res in res_dict:
         # activity information depends on if storage or component
-        var = getattr(m, f'{comp.name}_production')
         r = res_dict[res]
-        if comp.get_interaction().is_type('Storage'):
-          # Storages store LEVELS not ACTIVITIES, so calculate activity
-          # Production rate for storage defined as R_k = (L_{k+1} - L_k) / dt
-          if t > 0:
-            previous = var[r, t-1]
-            dt = m.Times[t] - m.Times[t-1]
-          else:
-            # FIXME check this with a variety of ValuedParams
-            previous = initial_storage[comp] # comp.get_interaction().get_initial_level(meta)
-            dt = m.Times[1] - m.Times[0]
-          new = var[r, t]
-          production = -1 * (new - previous) / dt # swap sign b/c negative is absorbing, positive is emitting
+        intr = comp.get_interaction()
+        if intr.is_type('Storage'):
+          # Storages have 3 variables: level, charge, and discharge
+          # -> so calculate activity
+          charge = getattr(m, f'{comp.name}_charge')
+          discharge = getattr(m, f'{comp.name}_discharge')
+          # note that "charge" is negative (as it's consuming) and discharge is positive
+          # -> so the intuitive |discharge| - |charge| becomes discharge + charge
+          production = discharge[r, t] + charge[r, t]
         else:
+          var = getattr(m, f'{comp.name}_production')
           # TODO move to this? balance += m._activity.get_activity(comp, res, t)
           production = var[r, t]
         balance += production
@@ -732,14 +966,15 @@ def _debug_print_soln(self, m):
     for c, comp in enumerate(m.Components):
       name = comp.name
       print('  component:', c, name)
-      for res, r in m.resource_index_map[comp].items():
-        print('    resource:', r, res)
-        for t, time in enumerate(m.Times):
-          prod = getattr(m, '{n}_production'.format(n=name))
-          if isinstance(prod, pyo.Var):
-            print('      time:', t + m.time_offset, time, prod[r, t].value)
-          elif isinstance(prod, pyo.Param):
-            print('      time:', t + m.time_offset, time, prod[r, t])
+      for tracker in comp.get_tracking_vars():
+        for res, r in m.resource_index_map[comp].items():
+          print(f'    tracker: {tracker} resource {r}: {res}')
+          for t, time in enumerate(m.Times):
+            prod = getattr(m, f'{name}_{tracker}')
+            if isinstance(prod, pyo.Var):
+              print('      time:', t + m.time_offset, time, prod[r, t].value)
+            elif isinstance(prod, pyo.Param):
+              print('      time:', t + m.time_offset, time, prod[r, t])
     print('*'*80)
 
 
@@ -769,10 +1004,11 @@ def initialize(self, components, resources_map, times, model):
     DispatchState.initialize(self, components, resources_map, times)
     self._model = model
 
-  def get_activity_indexed(self, comp, r, t, valued=True, **kwargs):
+  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
@@ -780,7 +1016,7 @@ def get_activity_indexed(self, comp, r, t, valued=True, **kwargs):
       @ 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}_production')[r, t]
+    prod = getattr(self._model, f'{comp.name}_{activity}')[r, t]
     if valued:
       return prod()
     return prod
diff --git a/src/input_loader.py b/src/input_loader.py
index a378d0ff..041719c4 100644
--- a/src/input_loader.py
+++ b/src/input_loader.py
@@ -43,45 +43,44 @@ def parse(xml, loc, messageHandler):
 
   # intentionally read case first
   case_node = xml.find('Case')
+  if case_node is None:
+    raise IOError('<Case> node is missing from HERON input file!')
   case = Cases.Case(loc, messageHandler=messageHandler)
   case.read_input(case_node)
 
-  # read everything else
-  for section in xml:
-    if section.tag == 'Components':
-      for comp_xml in section:
-        #print(type(Components.Component()))
-        #time.sleep(2000)
-        comp = Components.Component(messageHandler=messageHandler)
-        # check parsing
-        comp.read_input(comp_xml, case.get_mode())
-        components.append(comp)
+  # components
+  components_node = xml.find('Components')
+  if components_node is None:
+    raise IOError('<Components> node is missing from HERON input file!')
+  for comp_xml in components_node:
+    comp = Components.Component(messageHandler=messageHandler)
+    # check parsing
+    comp.read_input(comp_xml, case.get_mode())
+    components.append(comp)
+  if not components:
+    raise IOError('No <Component> nodes were found in the <Components> section!')
 
-    elif section.tag == 'DataGenerators':
-      # TODO only load the generators we need?? it'd be a good idea ...
-      for sub_xml in section:
-        typ = sub_xml.tag
-        if typ == 'CSV':
-          raise NotImplementedError('Not taking histories from CSV yet. If needed, let me know.')
-          new = Placeholders.CSV(messageHandler=messageHandler)
-        elif typ == 'ARMA':
-          new = Placeholders.ARMA(loc=loc, messageHandler=messageHandler)
-          #print("THIS IS INPUT LOADER")
-        elif typ == 'Function':
-          new = Placeholders.Function(loc=loc, messageHandler=messageHandler)
-        elif typ == 'ROM':
-          new = Placeholders.ROM(loc=loc, messageHandler=messageHandler)
-        else:
-          raise IOError('Unrecognized DataGenerator: "{}"'.format(sub_xml.tag))
-        new.read_input(sub_xml)
-        sources.append(new)
+  sources_node = xml.find('DataGenerators')
+  if sources_node is None:
+    raise IOError('<DataGenerators> node is missing from HERON input file!')
+  # TODO only load the generators we need?? it'd be a good idea ...
+  for sub_xml in sources_node:
+    typ = sub_xml.tag
+    if typ == 'CSV':
+      raise NotImplementedError('Not taking histories from CSV yet. If needed, let me know.')
+      # new = Placeholders.CSV(messageHandler=messageHandler)
+    elif typ == 'ARMA':
+      new = Placeholders.ARMA(loc=loc, messageHandler=messageHandler)
+    elif typ == 'Function':
+      new = Placeholders.Function(loc=loc, messageHandler=messageHandler)
+    elif typ == 'ROM':
+      new = Placeholders.ROM(loc=loc, messageHandler=messageHandler)
+    else:
+      raise IOError('Unrecognized DataGenerator: "{}"'.format(sub_xml.tag))
+    new.read_input(sub_xml)
+    sources.append(new)
 
   # now go back through and link up stuff
-  # TODO move to case.initialize?
-  need_source = (ValuedParams.factory.returnClass('Function'),
-                 ValuedParams.factory.returnClass('ARMA'),
-                 ValuedParams.factory.returnClass('ROM')
-                 )
   for comp in components:
     found = {}
     for obj, info in comp.get_crossrefs().items():
diff --git a/src/validators/ExampleValidator.py b/src/validators/ExampleValidator.py
index 7d804147..b949d64b 100644
--- a/src/validators/ExampleValidator.py
+++ b/src/validators/ExampleValidator.py
@@ -71,23 +71,24 @@ def validate(self, components, dispatch, times, meta):
     """
     errs = [] # TODO best format for this?
     for comp, info in dispatch._resources.items():
-      for res in info:
-        for t, time in enumerate(times):
-          current = dispatch.get_activity(comp, res, time)
-          if comp.get_interaction().is_type('Storage') and t == 0:
-            init_level = comp.get_interaction().get_initial_level(meta)
-          if t > 0:
-            previous = dispatch.get_activity(comp, res, times[t-1])
-            delta = current - previous
-            sign = np.sign(delta)
-            if abs(delta) - self._allowable > self._tolerance:
-              errs.append({'msg': f'Exceeded ramp of {self._allowable} with {delta:1.8e}',
-                           'limit': previous + (sign * self._allowable),
-                           'limit_type': 'lower' if (sign < 0) else 'upper',
-                           'component': comp,
-                           'resource': res,
-                           'time': time,
-                           'time_index': t,
-                          })
+      for tracker in comp.get_tracking_vars():
+        for res in info:
+          for t, time in enumerate(times):
+            current = dispatch.get_activity(comp, tracker, res, time)
+            if comp.get_interaction().is_type('Storage') and t == 0:
+              init_level = comp.get_interaction().get_initial_level(meta)
+            if t > 0:
+              previous = dispatch.get_activity(comp, tracker, res, times[t-1])
+              delta = current - previous
+              sign = np.sign(delta)
+              if abs(delta) - self._allowable > self._tolerance:
+                errs.append({'msg': f'Exceeded ramp of {self._allowable} with {delta:1.8e}',
+                             'limit': previous + (sign * self._allowable),
+                             'limit_type': 'lower' if (sign < 0) else 'upper',
+                             'component': comp,
+                             'resource': res,
+                             'time': time,
+                             'time_index': t,
+                            })
 
     return errs
diff --git a/templates/template_driver.py b/templates/template_driver.py
index 9b53d575..86eddde8 100644
--- a/templates/template_driver.py
+++ b/templates/template_driver.py
@@ -50,7 +50,7 @@ class Template(TemplateBase):
   TemplateBase.addNamingTemplates({'jobname'        : '{case}_{io}',
                                    'stepname'       : '{action}_{subject}',
                                    'variable'       : '{unit}_{feature}',
-                                   'dispatch'       : 'Dispatch__{component}__{resource}',
+                                   'dispatch'       : 'Dispatch__{component}__{tracker}__{resource}',
                                    'data object'    : '{source}_{contents}',
                                    'distribution'   : '{unit}_{feature}_dist',
                                    'ARMA sampler'   : '{rom}_sampler',
@@ -253,10 +253,10 @@ def _modify_outer_vargroups(self, template, case, components, sources):
       group = var_groups.find(".//Group[@name='GRO_outer_debug_dispatch']")
       for component in components:
         name = component.name
-        interaction = component.get_interaction()
-        for resource in interaction.get_resources():
-          var_name = self.namingTemplates['dispatch'].format(component=name, resource=resource)
-          self._updateCommaSeperatedList(group, var_name)
+        for tracker in component.get_tracking_vars():
+          for resource in component.get_resources():
+            var_name = self.namingTemplates['dispatch'].format(component=name, tracker=tracker, resource=resource)
+            self._updateCommaSeperatedList(group, var_name)
       # -> synthetic histories?
       group = var_groups.find(".//Group[@name='GRO_outer_debug_synthetics']")
       for source in sources:
@@ -719,10 +719,11 @@ def _modify_inner_components(self, template, case, components):
       else:
         raise NotImplementedError('Capacity from "{}" not implemented yet. Component: {}'.format(capacity, cap_name))
 
-      for resource in interaction.get_resources():
-        var_name = self.namingTemplates['dispatch'].format(component=name, resource=resource)
-        self._updateCommaSeperatedList(groups['init_disp'], var_name)
-        self._updateCommaSeperatedList(groups['full_dispatch'], var_name)
+      for tracker in component.get_tracking_vars():
+        for resource in component.get_resources():
+          var_name = self.namingTemplates['dispatch'].format(component=name, tracker=tracker, resource=resource)
+          self._updateCommaSeperatedList(groups['init_disp'], var_name)
+          self._updateCommaSeperatedList(groups['full_dispatch'], var_name)
 
   def _modify_inner_debug(self, template, case, components):
     """


Test patch:
diff --git a/src/dispatch/twin_pyomo_test_ch_disch.py b/src/dispatch/twin_pyomo_test_ch_disch.py
new file mode 100644
index 00000000..8bb9883d
--- /dev/null
+++ b/src/dispatch/twin_pyomo_test_ch_disch.py
@@ -0,0 +1,253 @@
+# Copyright 2020, Battelle Energy Alliance, LLC
+# ALL RIGHTS RESERVED
+"""
+Enables quick testing of Pyomo problem solves, intending to set up a system
+similar to how it is set up in the Pyomo dispatcher. This allows rapid testing
+of different configurations for the rolling window optimization.
+"""
+import platform
+
+import numpy as np
+
+import pyomo.environ as pyo
+from pyomo.opt import SolverStatus, TerminationCondition
+
+if platform.system() == 'Windows':
+  SOLVER = 'glpk'
+else:
+  SOLVER = 'cbc'
+
+# setup stuff
+components = ['steam_source', 'elec_generator', 'steam_storage', 'elec_sink']
+resources = ['steam', 'electricity']
+time = np.linspace(0, 10, 11) # from @1 to @2 in @3 steps
+dt = time[1] - time[0]
+resource_map = {'steam_source': {'steam': 0},
+                'elec_generator': {'steam': 0, 'electricity': 1},
+                'steam_storage': {'steam': 0},
+                'elec_sink': {'electricity': 0},
+                }
+activity = {}
+for comp in components:
+  activity[comp] = np.zeros((len(resources), len(time)), dtype=float)
+
+# sizing specifications
+storage_initial = 50 # kg of steam
+storage_limit = 400 # kg of steam
+steam_produced = 100 # kg/h of steam
+gen_consume_limit = 110 # consumes at most 110 kg/h steam
+sink_limit = 10000 # kWh/h = kW of electricity
+
+def make_concrete_model():
+  """
+    Test writing a simple concrete model with terms typical to the pyomo dispatcher.
+    @ In, None
+    @ Out, m, pyo.ConcreteModel, instance of the model to solve
+  """
+  m = pyo.ConcreteModel()
+  # indices
+  C = np.arange(0, len(components), dtype=int) # indexes component
+  R = np.arange(0, len(resources), dtype=int)  # indexes resources
+  T = np.arange(0, len(time), dtype=int)       # indexes time
+  # move onto model
+  m.C = pyo.Set(initialize=C)
+  m.R = pyo.Set(initialize=R)
+  m.T = pyo.Set(initialize=T)
+  # store some stuff for reference -> NOT NOTICED by Pyomo, we hope
+  m.Times = time
+  m.Components = components
+  m.resource_index_map = resource_map
+  m.Activity = activity
+  #*******************
+  #  set up optimization variables
+  # -> for now we just do this manually
+  # steam_source
+  m.steam_source_index_map = pyo.Set(initialize=range(len(m.resource_index_map['steam_source'])))
+  m.steam_source_production = pyo.Var(m.steam_source_index_map, m.T, initialize=steam_produced, bounds=(steam_produced, steam_produced))
+  # elec_generator
+  m.elec_generator_index_map = pyo.Set(initialize=range(len(m.resource_index_map['elec_generator'])))
+  m.elec_generator_production = pyo.Var(m.elec_generator_index_map, m.T, initialize=0, bounds=(-gen_consume_limit, 0))
+  # steam_storage
+  m.steam_storage_index_map = pyo.Set(initialize=range(len(m.resource_index_map['steam_storage'])))
+  m.steam_storage_level = pyo.Var(m.steam_storage_index_map, m.T, initialize=0, bounds=(0, storage_limit))
+  m.steam_storage_charge = pyo.Var(m.steam_storage_index_map, m.T, initialize=0, bounds=(-storage_limit/dt, 0))
+  m.steam_storage_discharge = pyo.Var(m.steam_storage_index_map, m.T, initialize=0, bounds=(0, storage_limit/dt))
+  # elec_sink
+  m.elec_sink_index_map = pyo.Set(initialize=range(len(m.resource_index_map['elec_sink'])))
+  m.elec_sink_production = pyo.Var(m.elec_sink_index_map, m.T, initialize=0, bounds=(-sink_limit, 0))
+  #*******************
+  #  set up lower, upper bounds
+  # -> for now we just do this manually
+  # -> consuming is negative sign by convention!
+  # -> producing is positive sign by convention!
+  # steam source produces exactly 100 steam
+  #    m.steam_source_lower_limit = pyo.Constraint(m.T, rule=lambda m, t: m.steam_source_production[0, t] >= steam_produced)
+  #    m.steam_source_upper_limit = pyo.Constraint(m.T, rule=lambda m, t: m.steam_source_production[0, t] <= steam_produced)
+  # elec generator can consume steam to produce electricity; 0 < consumed steam < 1000
+  # -> this effectively limits electricity production, but we're defining capacity in steam terms for fun
+  # -> therefore signs are negative, -1000 < consumed steam < 0!
+  #    m.elec_generator_lower_limit = pyo.Constraint(m.T, rule=lambda m, t: m.elec_generator_production[0, t] >= -gen_consume_limit)
+  #    m.elec_generator_upper_limit = pyo.Constraint(m.T, rule=lambda m, t: m.elec_generator_production[0, t] <= 0)
+  # elec sink can take any amount of electricity
+  # -> consuming, so -10000 < consumed elec < 0
+  #    m.elec_sink_lower_limit = pyo.Constraint(m.T, rule=lambda m, t: m.elec_sink_production[0, t] >= -sink_limit)
+  #    m.elec_sink_upper_limit = pyo.Constraint(m.T, rule=lambda m, t: m.elec_sink_production[0, t] <= 0)
+  # storage is in LEVEL not ACTIVITY (e.g. kg not kg/s) -> lets say it can store X kg
+  #    m.steam_storage_lower_limit = pyo.Constraint(m.T, rule=lambda m, t: m.steam_storage_level[0, t] >= 0)
+  #    m.steam_storage_upper_limit = pyo.Constraint(m.T, rule=lambda m, t: m.steam_storage_level[0, t] <= storage_limit)
+  #*******************
+  # create transfer function
+  # 2 steam make 1 electricity (sure, why not)
+  m.elec_generator_transfer = pyo.Constraint(m.T, rule=_generator_transfer)
+  #*******************
+  # create conservation rules
+  # steam
+  m.steam_conservation = pyo.Constraint(m.T, rule=_conserve_steam)
+  # electricity
+  m.elec_conservation = pyo.Constraint(m.T, rule=_conserve_electricity)
+  #*******************
+  # create objective function
+  m.OBJ = pyo.Objective(sense=pyo.maximize, rule=_economics)
+  #######
+  # return
+  return m
+
+#######
+#
+# Callback Functions
+#
+def _generator_transfer(m, t):
+  """
+    Constraint rule for electricity generation in generator
+    @ In, m, pyo.ConcreteModel, model containing problem
+    @ In, t, int, time indexer
+    @ Out, constraint, bool, constraining evaluation
+  """
+  return - m.elec_generator_production[0, t] == 2.0 * m.elec_generator_production[1, t]
+
+def _conserve_steam(m, t):
+  """
+    Constraint rule for conserving steam
+    @ In, m, pyo.ConcreteModel, model containing problem
+    @ In, t, int, time indexer
+    @ Out, constraint, bool, constraining evaluation
+  """
+  # signs are tricky here, consumption is negative and production is positive
+  # a positive delta in steam storage level means it absorbed steam, so it's a negative term
+  storage_source = - (m.steam_storage_level[0, t] - (storage_initial if t == 0 else (m.steam_storage_charge[0, t-1] + m.steam_storage_discharge[0, t-1]))) / dt
+  sources = storage_source + m.steam_source_production[0, t]
+  sinks = m.elec_generator_production[0, t]
+  return sources + sinks == 0
+
+def _conserve_electricity(m, t):
+  """
+    Constraint rule for conserving electricity
+    @ In, m, pyo.ConcreteModel, model containing problem
+    @ In, t, int, time indexer
+    @ Out, constraint, bool, constraining evaluation
+  """
+  sources = m.elec_generator_production[1, t]
+  sinks = m.elec_sink_production[0, t]
+  return sources + sinks == 0
+
+def _economics(m):
+  """
+    Constraint rule for optimization target
+    @ In, m, pyo.ConcreteModel, model containing problem
+    @ Out, objective, float, constraining evaluation
+  """
+  opex = sum(m.elec_generator_production[0, t] for t in m.T) * 10 # will be negative b/c consumed
+  sales = - sum((m.elec_sink_production[0, t] * (100 if t < 5 else 1)) for t in m.T) # net positive because consumed
+  return opex + sales
+
+#######
+#
+# Debug printing functions
+#
+def print_setup(m):
+  """
+    Debug printing for pre-solve model setup
+    @ In, m, pyo.ConcreteModel, model containing problem
+    @ Out, None
+  """
+  print('/' + '='*80)
+  print('DEBUGG model pieces:')
+  print('  -> objective:')
+  print('     ', m.OBJ.pprint())
+  print('  -> variables:')
+  for var in m.component_objects(pyo.Var):
+    print('     ', var.pprint())
+  print('  -> constraints:')
+  for constr in m.component_objects(pyo.Constraint):
+    print('     ', constr.pprint())
+  print('\\' + '='*80)
+  print('')
+
+def print_solution(m):
+  """
+    Debug printing for post-solve model setup
+    @ In, m, pyo.ConcreteModel, model containing problem
+    @ Out, None
+  """
+  print('')
+  print('*'*80)
+  print('solution:')
+  print('  objective value:', m.OBJ())
+  print('time | steam source | storage level | elec gen (s, e) | elec sink')
+  for t in m.T:
+    print(f'{m.Times[t]:1.2e} | ' +
+        f'{m.steam_source_production[0, t].value: 1.3e} | ' +
+        f'{m.steam_storage_level[0, t].value: 1.3e} | ' +
+        f'({m.elec_generator_production[0, t].value: 1.3e}, {m.elec_generator_production[1, t].value: 1.3e}) | ' +
+        f'{m.elec_sink_production[0, t].value: 1.3e}'
+        )
+  print('*'*80)
+
+#######
+#
+# Solver.
+#
+def solve_model(m):
+  """
+    Solves the model.
+    @ In, m, pyo.ConcreteModel, model containing problem
+    @ Out, m, pyo.ConcreteModel, results
+  """
+  soln = pyo.SolverFactory(SOLVER).solve(m)
+  return soln
+
+if __name__ == '__main__':
+  m = make_concrete_model()
+  print_setup(m)
+  s = solve_model(m)
+  print_solution(m)
+
+# solution using setup:
+#   time = np.linspace(0, 10, 11)
+#   storage_initial = 50 # kg of steam
+#   storage_limit = 400 # kg of steam
+#   steam_produced = 100 # kg/h of steam
+#   gen_consume_limit = 110 # consumes at most 110 kg/h steam
+#   sink_limit = 10000 # kWh/h = kW of electricity
+#   1 steam = 2 * electricity
+#   cost for generator = 10 * kg/h steam consumed
+#   profit = 100 * electricity consumed at sink, t < 5,
+#              1 * electricity consumed at sink, t >= 5,
+#
+# should look like:
+# ********************************************************************************
+# solution:
+#   objective value: 20100.0
+# time | steam source | steam storage | elec gen (s, e) | elec sink
+# 0.00e+00 |  1.000e+02 |  4.000e+01 | (-1.100e+02,  5.500e+01) | -5.500e+01
+# 1.00e+00 |  1.000e+02 |  3.000e+01 | (-1.100e+02,  5.500e+01) | -5.500e+01
+# 2.00e+00 |  1.000e+02 |  2.000e+01 | (-1.100e+02,  5.500e+01) | -5.500e+01
+# 3.00e+00 |  1.000e+02 |  1.000e+01 | (-1.100e+02,  5.500e+01) | -5.500e+01
+# 4.00e+00 |  1.000e+02 |  0.000e+00 | (-1.100e+02,  5.500e+01) | -5.500e+01
+# 5.00e+00 |  1.000e+02 |  0.000e+00 | (-1.000e+02,  5.000e+01) | -5.000e+01
+# 6.00e+00 |  1.000e+02 |  0.000e+00 | (-1.000e+02,  5.000e+01) | -5.000e+01
+# 7.00e+00 |  1.000e+02 |  1.000e+02 | ( 0.000e+00,  0.000e+00) |  0.000e+00
+# 8.00e+00 |  1.000e+02 |  2.000e+02 | ( 0.000e+00,  0.000e+00) |  0.000e+00
+# 9.00e+00 |  1.000e+02 |  3.000e+02 | ( 0.000e+00,  0.000e+00) |  0.000e+00
+# 1.00e+01 |  1.000e+02 |  4.000e+02 | ( 0.000e+00,  0.000e+00) |  0.000e+00
+# ********************************************************************************
diff --git a/src/dispatch/twin_pyomo_test_rte.py b/src/dispatch/twin_pyomo_test_rte.py
new file mode 100644
index 00000000..bba3afa0
--- /dev/null
+++ b/src/dispatch/twin_pyomo_test_rte.py
@@ -0,0 +1,281 @@
+# Copyright 2020, Battelle Energy Alliance, LLC
+# ALL RIGHTS RESERVED
+"""
+Enables quick testing of Pyomo problem solves, intending to set up a system
+similar to how it is set up in the Pyomo dispatcher. This allows rapid testing
+of different configurations for the rolling window optimization.
+"""
+import platform
+
+import numpy as np
+
+import pyomo.environ as pyo
+from pyomo.opt import SolverStatus, TerminationCondition
+
+if platform.system() == 'Windows':
+  SOLVER = 'glpk'
+else:
+  SOLVER = 'cbc'
+
+# setup stuff
+components = ['steam_source', 'elec_generator', 'steam_storage', 'elec_sink']
+resources = ['steam', 'electricity']
+time = np.linspace(0, 10, 11) # from @1 to @2 in @3 steps
+dt = time[1] - time[0]
+resource_map = {'steam_source': {'steam': 0},
+                'elec_generator': {'steam': 0, 'electricity': 1},
+                'steam_storage': {'steam': 0},
+                'elec_sink': {'electricity': 0},
+                }
+activity = {}
+for comp in components:
+  activity[comp] = np.zeros((len(resources), len(time)), dtype=float)
+
+# sizing specifications
+storage_initial = 50    # kg of steam
+storage_limit = 400     # kg of steam
+steam_produced = 100    # kg/h of steam
+gen_consume_limit = 110 # consumes at most 110 kg/h steam
+sink_limit = 10000      # kWh/h = kW of electricity
+rte = 0.90              # storage round trip efficiency
+sq_rte = np.sqrt(rte)   # square root of rte
+
+def make_concrete_model():
+  """
+    Test writing a simple concrete model with terms typical to the pyomo dispatcher.
+    @ In, None
+    @ Out, m, pyo.ConcreteModel, instance of the model to solve
+  """
+  m = pyo.ConcreteModel()
+  # indices
+  C = np.arange(0, len(components), dtype=int) # indexes component
+  R = np.arange(0, len(resources), dtype=int)  # indexes resources
+  T = np.arange(0, len(time), dtype=int)       # indexes time
+  # move onto model
+  m.C = pyo.Set(initialize=C)
+  m.R = pyo.Set(initialize=R)
+  m.T = pyo.Set(initialize=T)
+  # store some stuff for reference -> NOT NOTICED by Pyomo, we hope
+  m.Times = time
+  m.Components = components
+  m.resource_index_map = resource_map
+  m.Activity = activity
+  #*******************
+  #  set up optimization variables
+  # -> for now we just do this manually
+  # steam_source
+  m.steam_source_index_map = pyo.Set(initialize=range(len(m.resource_index_map['steam_source'])))
+  m.steam_source_production = pyo.Var(m.steam_source_index_map, m.T, initialize=steam_produced, bounds=(steam_produced, steam_produced))
+  # elec_generator
+  m.elec_generator_index_map = pyo.Set(initialize=range(len(m.resource_index_map['elec_generator'])))
+  bounds = lambda m, r, t: ({0: -gen_consume_limit}.get(r, None), {0: 0}.get(r, None))
+  m.elec_generator_production = pyo.Var(m.elec_generator_index_map, m.T, initialize=0, bounds=bounds)
+  # steam_storage
+  m.steam_storage_index_map = pyo.Set(initialize=range(len(m.resource_index_map['steam_storage'])))
+  m.steam_storage_level = pyo.Var(m.steam_storage_index_map, m.T, initialize=0, bounds=(0, storage_limit))
+  m.steam_storage_charge = pyo.Var(m.steam_storage_index_map, m.T, initialize=0, bounds=(-storage_limit/dt, 0)) # TODO should be ramp rate limit
+  m.steam_storage_discharge = pyo.Var(m.steam_storage_index_map, m.T, initialize=0, bounds=(0, storage_limit/dt)) # TODO ramp rate limit
+  m.steam_oneway_bigM = pyo.Var(m.T, initialize=0, within=pyo.Binary) # 0 is charging, discharging is 1
+  large_eps = storage_limit / dt * 1.1
+  m.steam_oneway_charge = pyo.Constraint(m.T, rule=lambda m, t: -m.steam_storage_charge[0, t] <= (1 - m.steam_oneway_bigM[t]) * large_eps)
+  m.steam_oneway_discharge = pyo.Constraint(m.T, rule=lambda m, t: m.steam_storage_discharge[0, t] <= m.steam_oneway_bigM[t] * large_eps)
+  # OLD m.steam_storage_production = pyo.Var(m.steam_storage_index_map, m.T, initialize=0, bounds=(0, storage_limit))
+  # elec_sink
+  m.elec_sink_index_map = pyo.Set(initialize=range(len(m.resource_index_map['elec_sink'])))
+  m.elec_sink_production = pyo.Var(m.elec_sink_index_map, m.T, initialize=0, bounds=(-sink_limit, 0))
+  #*******************
+  #  set up lower, upper bounds
+  # -> for now we just do this manually
+  # -> consuming is negative sign by convention!
+  # -> producing is positive sign by convention!
+  # steam source produces exactly 100 steam
+  # OLD m.steam_source_lower_limit = pyo.Constraint(m.T, rule=lambda m, t: m.steam_source_production[0, t] >= steam_produced)
+  # OLD m.steam_source_upper_limit = pyo.Constraint(m.T, rule=lambda m, t: m.steam_source_production[0, t] <= steam_produced)
+  # elec generator can consume steam to produce electricity; 0 < consumed steam < 1000
+  # -> this effectively limits electricity production, but we're defining capacity in steam terms for fun
+  # -> therefore signs are negative, -1000 < consumed steam < 0!
+  # OLD m.elec_generator_lower_limit = pyo.Constraint(m.T, rule=lambda m, t: m.elec_generator_production[0, t] >= -gen_consume_limit)
+  # OLD m.elec_generator_upper_limit = pyo.Constraint(m.T, rule=lambda m, t: m.elec_generator_production[0, t] <= 0)
+  # elec sink can take any amount of electricity
+  # -> consuming, so -10000 < consumed elec < 0
+  # OLD m.elec_sink_lower_limit = pyo.Constraint(m.T, rule=lambda m, t: m.elec_sink_production[0, t] >= -sink_limit)
+  # OLD m.elec_sink_upper_limit = pyo.Constraint(m.T, rule=lambda m, t: m.elec_sink_production[0, t] <= 0)
+  # storage is in LEVEL not ACTIVITY (e.g. kg not kg/s) -> lets say it can store X kg
+  # OLD m.steam_storage_lower_limit = pyo.Constraint(m.T, rule=lambda m, t: m.steam_storage_production[0, t] >= 0)
+  # OLD m.steam_storage_upper_limit = pyo.Constraint(m.T, rule=lambda m, t: m.steam_storage_production[0, t] <= storage_limit)
+  #*******************
+  # create transfer function
+  # 2 steam make 1 electricity (sure, why not)
+  m.elec_generator_transfer = pyo.Constraint(m.T, rule=_generator_transfer)
+  #*******************
+  # create conservation rules
+  # steam
+  m.steam_conservation = pyo.Constraint(m.T, rule=_conserve_steam)
+  # electricity
+  m.elec_conservation = pyo.Constraint(m.T, rule=_conserve_electricity)
+  # track storage level by charge/discharge
+  m.steam_storage_management = pyo.Constraint(m.T, rule=_manage_storage)
+  #*******************
+  # create objective function
+  m.OBJ = pyo.Objective(sense=pyo.maximize, rule=_economics)
+  #######
+  # return
+  return m
+
+#######
+#
+# Callback Functions
+#
+def _generator_transfer(m, t):
+  """
+    Constraint rule for electricity generation in generator
+    @ In, m, pyo.ConcreteModel, model containing problem
+    @ In, t, int, time indexer
+    @ Out, constraint, bool, constraining evaluation
+  """
+  return - m.elec_generator_production[0, t] == 2.0 * m.elec_generator_production[1, t]
+
+def _conserve_steam(m, t):
+  """
+    Constraint rule for conserving steam
+    @ In, m, pyo.ConcreteModel, model containing problem
+    @ In, t, int, time indexer
+    @ Out, constraint, bool, constraining evaluation
+  """
+  # signs are tricky here, consumption is negative and production is positive
+  # a positive delta in steam storage level means it absorbed steam, so it's a negative term
+  storage_source = m.steam_storage_charge[0, t] + m.steam_storage_discharge[0, t] #(storage_initial if t == 0 else m.steam_storage_level[0, t-1])) / dt
+  sources = storage_source + m.steam_source_production[0, t]
+  sinks = m.elec_generator_production[0, t]
+  return sources + sinks == 0
+
+def _conserve_electricity(m, t):
+  """
+    Constraint rule for conserving electricity
+    @ In, m, pyo.ConcreteModel, model containing problem
+    @ In, t, int, time indexer
+    @ Out, constraint, bool, constraining evaluation
+  """
+  sources = m.elec_generator_production[1, t]
+  sinks = m.elec_sink_production[0, t]
+  return sources + sinks == 0
+
+def _manage_storage(m, t):
+  """
+    Balance level of the storage unit with charge, discharge activities
+    @ In, m, pyo.ConcreteModel, model containing problem
+    @ In, t, int, time indexer
+    @ Out, constraint, bool, constraining evaluation
+  """
+  if t > 0:
+    previous = m.steam_storage_level[0, t-1]
+  else:
+    previous = storage_initial
+  # recall charge is negative (absorbing) and discharge is positive (emitting)
+  production = -sq_rte * m.steam_storage_charge[0, t] - m.steam_storage_discharge[0, t] / sq_rte
+  return m.steam_storage_level[0, t] == previous + production * dt
+
+def _economics(m):
+  """
+    Constraint rule for optimization target
+    @ In, m, pyo.ConcreteModel, model containing problem
+    @ Out, objective, float, constraining evaluation
+  """
+  opex = sum(m.elec_generator_production[0, t] for t in m.T) * 10 # will be negative b/c consumed
+  sales = - sum((m.elec_sink_production[0, t] * (100 if t < 5 else 1)) for t in m.T) # net positive because consumed
+  return opex + sales
+
+#######
+#
+# Debug printing functions
+#
+def print_setup(m):
+  """
+    Debug printing for pre-solve model setup
+    @ In, m, pyo.ConcreteModel, model containing problem
+    @ Out, None
+  """
+  print('/' + '='*80)
+  print('DEBUGG model pieces:')
+  print('  -> objective:')
+  print('     ', m.OBJ.pprint())
+  print('  -> variables:')
+  for var in m.component_objects(pyo.Var):
+    print('     ', var.pprint())
+  print('  -> constraints:')
+  for constr in m.component_objects(pyo.Constraint):
+    print('     ', constr.pprint())
+  print('\\' + '='*80)
+  print('')
+
+def print_solution(m):
+  """
+    Debug printing for post-solve model setup
+    @ In, m, pyo.ConcreteModel, model containing problem
+    @ Out, None
+  """
+  print('')
+  print('*'*80)
+  print('solution:')
+  print('  objective value:', m.OBJ())
+  print('  storage initial:', storage_initial)
+  print('  dt:', dt)
+  print(f'{"time":^8} | {"steam src":^10} | {"storage c, d, l":^34} | {"elec gen (s, e)":^22} | elec sink | bigM')
+  for t in m.T:
+    print(f'{m.Times[t]:1.2e} | ' +
+        f'{m.steam_source_production[0, t].value: 1.3e} | ' +
+        f'{m.steam_storage_charge[0, t].value: 1.3e}, {m.steam_storage_discharge[0, t].value: 1.3e}, {m.steam_storage_level[0, t].value: 1.3e} | ' +
+        f'{m.elec_generator_production[0, t].value: 1.3e}, {m.elec_generator_production[1, t].value: 1.3e} | ' +
+        f'{m.elec_sink_production[0, t].value: 1.3e} | ' +
+        f'{m.steam_oneway_bigM[t].value} '
+        )
+  print('*'*80)
+
+#######
+#
+# Solver.
+#
+def solve_model(m):
+  """
+    Solves the model.
+    @ In, m, pyo.ConcreteModel, model containing problem
+    @ Out, m, pyo.ConcreteModel, results
+  """
+  soln = pyo.SolverFactory(SOLVER).solve(m)
+  return soln
+
+if __name__ == '__main__':
+  m = make_concrete_model()
+  print_setup(m)
+  s = solve_model(m)
+  print_solution(m)
+
+# solution using setup:
+#   time = np.linspace(0, 10, 11)
+#   storage_initial = 50 # kg of steam
+#   storage_limit = 400 # kg of steam
+#   steam_produced = 100 # kg/h of steam
+#   gen_consume_limit = 110 # consumes at most 110 kg/h steam
+#   sink_limit = 10000 # kWh/h = kW of electricity
+#   1 steam = 2 * electricity
+#   cost for generator = 10 * kg/h steam consumed
+#   profit = 100 * electricity consumed at sink, t < 5,
+#              1 * electricity consumed at sink, t >= 5,
+#
+# should look like:
+# ********************************************************************************
+# solution:
+#   objective value: 20100.0
+# time | steam source | steam storage | elec gen (s, e) | elec sink
+# 0.00e+00 |  1.000e+02 |  4.000e+01 | (-1.100e+02,  5.500e+01) | -5.500e+01
+# 1.00e+00 |  1.000e+02 |  3.000e+01 | (-1.100e+02,  5.500e+01) | -5.500e+01
+# 2.00e+00 |  1.000e+02 |  2.000e+01 | (-1.100e+02,  5.500e+01) | -5.500e+01
+# 3.00e+00 |  1.000e+02 |  1.000e+01 | (-1.100e+02,  5.500e+01) | -5.500e+01
+# 4.00e+00 |  1.000e+02 |  0.000e+00 | (-1.100e+02,  5.500e+01) | -5.500e+01
+# 5.00e+00 |  1.000e+02 |  0.000e+00 | (-1.000e+02,  5.000e+01) | -5.000e+01
+# 6.00e+00 |  1.000e+02 |  0.000e+00 | (-1.000e+02,  5.000e+01) | -5.000e+01
+# 7.00e+00 |  1.000e+02 |  1.000e+02 | ( 0.000e+00,  0.000e+00) |  0.000e+00
+# 8.00e+00 |  1.000e+02 |  2.000e+02 | ( 0.000e+00,  0.000e+00) |  0.000e+00
+# 9.00e+00 |  1.000e+02 |  3.000e+02 | ( 0.000e+00,  0.000e+00) |  0.000e+00
+# 1.00e+01 |  1.000e+02 |  4.000e+02 | ( 0.000e+00,  0.000e+00) |  0.000e+00
+# ********************************************************************************
diff --git a/tests/integration_tests/mechanics/cashflows/functions.py b/tests/integration_tests/mechanics/cashflows/functions.py
index 32fe8c35..900b0e8d 100644
--- a/tests/integration_tests/mechanics/cashflows/functions.py
+++ b/tests/integration_tests/mechanics/cashflows/functions.py
@@ -16,15 +16,3 @@ def capacity(data, meta):
   data = {'driver': c}
   return data, meta
 
-def activity(data, meta):
-  """
-    return usage of resource "a"
-    @ In, data, dict, data requeset
-    @ In, meta, dict, state information
-    @ Out, data, dict, filled data
-    @ Out, meta, dict, state information
-  """
-  a = meta['HERON']['activity']['a']
-  data = {'driver': a}
-  return data, meta
-
diff --git a/tests/integration_tests/mechanics/cashflows/heron_input.xml b/tests/integration_tests/mechanics/cashflows/heron_input.xml
index f58f954a..81f610da 100644
--- a/tests/integration_tests/mechanics/cashflows/heron_input.xml
+++ b/tests/integration_tests/mechanics/cashflows/heron_input.xml
@@ -25,7 +25,9 @@
         <verbosity>50</verbosity>
       </economics>
       <dispatcher>
-        <pyomo/>
+        <pyomo>
+          <solver>glpk</solver>
+        </pyomo>
       </dispatcher>
     </Case>
 
@@ -71,7 +73,7 @@
           </CashFlow>
           <CashFlow name="VOM" type="repeating" taxable="False" inflation="none" mult_target="False">
             <driver>
-              <Function method="activity">functions</Function>
+              <activity>a</activity>
             </driver>
             <reference_price>
               <fixed_value>-1</fixed_value>
@@ -90,7 +92,7 @@
           <lifetime>30</lifetime>
           <CashFlow name="sales" type="repeating" taxable="False" inflation="none" mult_target="False">
             <driver>
-              <Function method="activity">functions</Function>
+              <activity>a</activity>
             </driver>
             <reference_price>
               <fixed_value>10.0</fixed_value>
diff --git a/tests/integration_tests/mechanics/debug_mode/gold/dispatch.nc b/tests/integration_tests/mechanics/debug_mode/gold/dispatch.nc
index 81dd8fe1..e0e869fb 100644
Binary files a/tests/integration_tests/mechanics/debug_mode/gold/dispatch.nc and b/tests/integration_tests/mechanics/debug_mode/gold/dispatch.nc differ
diff --git a/tests/integration_tests/mechanics/debug_mode/gold/dispatch_print.csv b/tests/integration_tests/mechanics/debug_mode/gold/dispatch_print.csv
index f3687c39..c627731b 100644
--- a/tests/integration_tests/mechanics/debug_mode/gold/dispatch_print.csv
+++ b/tests/integration_tests/mechanics/debug_mode/gold/dispatch_print.csv
@@ -1,127 +1,127 @@
-RAVEN_sample_ID,Time,Year,_ROM_Cluster,scaling,Dispatch__steamer__steam,Dispatch__generator__steam,Dispatch__generator__electricity,Dispatch__electr_market__electricity,Dispatch__electr_flex__electricity,Signal,ProbabilityWeight,prefix,PointProbability,ProbabilityWeight-steamer_capacity
-0,0.0,10,0,1.0,63.808135,-63.808135,31.904067,0.0,-31.904067,-2.51201283814e-10,1.0,1_0,0.010101010101,1.0
-0,0.0,11,0,1.0,63.808135,-63.808135,31.904067,0.0,-31.904067,-2.51201283814e-10,1.0,1_0,0.010101010101,1.0
-0,0.0,12,0,1.0,63.808135,-63.808135,31.904067,0.0,-31.904067,-2.51201283814e-10,1.0,1_0,0.010101010101,1.0
-0,0.1,10,0,1.0,63.808135,-63.808135,31.904067,0.0,-31.904067,0.0627905200127,1.0,1_0,0.010101010101,1.0
-0,0.1,11,0,1.0,63.808135,-63.808135,31.904067,0.0,-31.904067,0.0627905200127,1.0,1_0,0.010101010101,1.0
-0,0.1,12,0,1.0,63.808135,-63.808135,31.904067,0.0,-31.904067,0.0627905200127,1.0,1_0,0.010101010101,1.0
-0,0.2,10,0,1.0,63.808135,-63.808135,31.904067,0.0,-31.904067,0.125333234008,1.0,1_0,0.010101010101,1.0
-0,0.2,11,0,1.0,63.808135,-63.808135,31.904067,0.0,-31.904067,0.125333234008,1.0,1_0,0.010101010101,1.0
-0,0.2,12,0,1.0,63.808135,-63.808135,31.904067,0.0,-31.904067,0.125333234008,1.0,1_0,0.010101010101,1.0
-0,0.3,10,0,1.0,63.808135,-63.808135,31.904067,0.0,-31.904067,0.187381314977,1.0,1_0,0.010101010101,1.0
-0,0.3,11,0,1.0,63.808135,-63.808135,31.904067,0.0,-31.904067,0.187381314977,1.0,1_0,0.010101010101,1.0
-0,0.3,12,0,1.0,63.808135,-63.808135,31.904067,0.0,-31.904067,0.187381314977,1.0,1_0,0.010101010101,1.0
-0,0.4,10,0,1.0,63.808135,-63.808135,31.904067,0.0,-31.904067,0.248689886838,1.0,1_0,0.010101010101,1.0
-0,0.4,11,0,1.0,63.808135,-63.808135,31.904067,0.0,-31.904067,0.248689886838,1.0,1_0,0.010101010101,1.0
-0,0.4,12,0,1.0,63.808135,-63.808135,31.904067,0.0,-31.904067,0.248689886838,1.0,1_0,0.010101010101,1.0
-0,0.5,10,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.309016994721,1.0,1_0,0.010101010101,1.0
-0,0.5,11,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.309016994721,1.0,1_0,0.010101010101,1.0
-0,0.5,12,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.309016994721,1.0,1_0,0.010101010101,1.0
-0,0.6,10,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.368124552404,1.0,1_0,0.010101010101,1.0
-0,0.6,11,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.368124552404,1.0,1_0,0.010101010101,1.0
-0,0.6,12,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.368124552404,1.0,1_0,0.010101010101,1.0
-0,0.7,10,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.42577929116,1.0,1_0,0.010101010101,1.0
-0,0.7,11,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.42577929116,1.0,1_0,0.010101010101,1.0
-0,0.7,12,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.42577929116,1.0,1_0,0.010101010101,1.0
-0,0.8,10,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.481753674692,1.0,1_0,0.010101010101,1.0
-0,0.8,11,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.481753674692,1.0,1_0,0.010101010101,1.0
-0,0.8,12,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.481753674692,1.0,1_0,0.010101010101,1.0
-0,0.9,10,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.535826794514,1.0,1_0,0.010101010101,1.0
-0,0.9,11,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.535826794514,1.0,1_0,0.010101010101,1.0
-0,0.9,12,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.535826794514,1.0,1_0,0.010101010101,1.0
-0,1.0,10,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.587785252891,1.0,1_0,0.010101010101,1.0
-0,1.0,11,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.587785252891,1.0,1_0,0.010101010101,1.0
-0,1.0,12,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.587785252891,1.0,1_0,0.010101010101,1.0
-0,1.1,10,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.637423990329,1.0,1_0,0.010101010101,1.0
-0,1.1,11,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.637423990329,1.0,1_0,0.010101010101,1.0
-0,1.1,12,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.637423990329,1.0,1_0,0.010101010101,1.0
-0,1.2,10,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.684547106263,1.0,1_0,0.010101010101,1.0
-0,1.2,11,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.684547106263,1.0,1_0,0.010101010101,1.0
-0,1.2,12,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.684547106263,1.0,1_0,0.010101010101,1.0
-0,1.3,10,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.728968626879,1.0,1_0,0.010101010101,1.0
-0,1.3,11,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.728968626879,1.0,1_0,0.010101010101,1.0
-0,1.3,12,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.728968626879,1.0,1_0,0.010101010101,1.0
-0,1.4,10,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.770513243013,1.0,1_0,0.010101010101,1.0
-0,1.4,11,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.770513243013,1.0,1_0,0.010101010101,1.0
-0,1.4,12,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.770513243013,1.0,1_0,0.010101010101,1.0
-0,1.5,10,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.809016994001,1.0,1_0,0.010101010101,1.0
-0,1.5,11,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.809016994001,1.0,1_0,0.010101010101,1.0
-0,1.5,12,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.809016994001,1.0,1_0,0.010101010101,1.0
-0,1.6,10,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.844327925978,1.0,1_0,0.010101010101,1.0
-0,1.6,11,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.844327925978,1.0,1_0,0.010101010101,1.0
-0,1.6,12,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.844327925978,1.0,1_0,0.010101010101,1.0
-0,1.7,10,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.87630668031,1.0,1_0,0.010101010101,1.0
-0,1.7,11,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.87630668031,1.0,1_0,0.010101010101,1.0
-0,1.7,12,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.87630668031,1.0,1_0,0.010101010101,1.0
-0,1.8,10,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.904827051898,1.0,1_0,0.010101010101,1.0
-0,1.8,11,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.904827051898,1.0,1_0,0.010101010101,1.0
-0,1.8,12,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.904827051898,1.0,1_0,0.010101010101,1.0
-0,1.9,10,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.929776485406,1.0,1_0,0.010101010101,1.0
-0,1.9,11,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.929776485406,1.0,1_0,0.010101010101,1.0
-0,1.9,12,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.929776485406,1.0,1_0,0.010101010101,1.0
-0,2.0,10,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.951056516745,1.0,1_0,0.010101010101,1.0
-0,2.0,11,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.951056516745,1.0,1_0,0.010101010101,1.0
-0,2.0,12,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.951056516745,1.0,1_0,0.010101010101,1.0
-1,0.0,10,0,1.0,63.808135,-63.808135,31.904067,0.0,-31.904067,8.34672934618e-10,1.0,1_1,0.010101010101,1.0
-1,0.0,11,0,1.0,63.808135,-63.808135,31.904067,0.0,-31.904067,8.34672934618e-10,1.0,1_1,0.010101010101,1.0
-1,0.0,12,0,1.0,63.808135,-63.808135,31.904067,0.0,-31.904067,8.34672934618e-10,1.0,1_1,0.010101010101,1.0
-1,0.1,10,0,1.0,63.808135,-63.808135,31.904067,0.0,-31.904067,0.0627905194607,1.0,1_1,0.010101010101,1.0
-1,0.1,11,0,1.0,63.808135,-63.808135,31.904067,0.0,-31.904067,0.0627905194607,1.0,1_1,0.010101010101,1.0
-1,0.1,12,0,1.0,63.808135,-63.808135,31.904067,0.0,-31.904067,0.0627905194607,1.0,1_1,0.010101010101,1.0
-1,0.2,10,0,1.0,63.808135,-63.808135,31.904067,0.0,-31.904067,0.125333233727,1.0,1_1,0.010101010101,1.0
-1,0.2,11,0,1.0,63.808135,-63.808135,31.904067,0.0,-31.904067,0.125333233727,1.0,1_1,0.010101010101,1.0
-1,0.2,12,0,1.0,63.808135,-63.808135,31.904067,0.0,-31.904067,0.125333233727,1.0,1_1,0.010101010101,1.0
-1,0.3,10,0,1.0,63.808135,-63.808135,31.904067,0.0,-31.904067,0.187381314472,1.0,1_1,0.010101010101,1.0
-1,0.3,11,0,1.0,63.808135,-63.808135,31.904067,0.0,-31.904067,0.187381314472,1.0,1_1,0.010101010101,1.0
-1,0.3,12,0,1.0,63.808135,-63.808135,31.904067,0.0,-31.904067,0.187381314472,1.0,1_1,0.010101010101,1.0
-1,0.4,10,0,1.0,63.808135,-63.808135,31.904067,0.0,-31.904067,0.248689887657,1.0,1_1,0.010101010101,1.0
-1,0.4,11,0,1.0,63.808135,-63.808135,31.904067,0.0,-31.904067,0.248689887657,1.0,1_1,0.010101010101,1.0
-1,0.4,12,0,1.0,63.808135,-63.808135,31.904067,0.0,-31.904067,0.248689887657,1.0,1_1,0.010101010101,1.0
-1,0.5,10,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.309016994562,1.0,1_1,0.010101010101,1.0
-1,0.5,11,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.309016994562,1.0,1_1,0.010101010101,1.0
-1,0.5,12,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.309016994562,1.0,1_1,0.010101010101,1.0
-1,0.6,10,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.368124553372,1.0,1_1,0.010101010101,1.0
-1,0.6,11,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.368124553372,1.0,1_1,0.010101010101,1.0
-1,0.6,12,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.368124553372,1.0,1_1,0.010101010101,1.0
-1,0.7,10,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.425779291511,1.0,1_1,0.010101010101,1.0
-1,0.7,11,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.425779291511,1.0,1_1,0.010101010101,1.0
-1,0.7,12,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.425779291511,1.0,1_1,0.010101010101,1.0
-1,0.8,10,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.481753674744,1.0,1_1,0.010101010101,1.0
-1,0.8,11,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.481753674744,1.0,1_1,0.010101010101,1.0
-1,0.8,12,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.481753674744,1.0,1_1,0.010101010101,1.0
-1,0.9,10,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.535826794514,1.0,1_1,0.010101010101,1.0
-1,0.9,11,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.535826794514,1.0,1_1,0.010101010101,1.0
-1,0.9,12,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.535826794514,1.0,1_1,0.010101010101,1.0
-1,1.0,10,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.587785251807,1.0,1_1,0.010101010101,1.0
-1,1.0,11,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.587785251807,1.0,1_1,0.010101010101,1.0
-1,1.0,12,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.587785251807,1.0,1_1,0.010101010101,1.0
-1,1.1,10,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.637423989248,1.0,1_1,0.010101010101,1.0
-1,1.1,11,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.637423989248,1.0,1_1,0.010101010101,1.0
-1,1.1,12,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.637423989248,1.0,1_1,0.010101010101,1.0
-1,1.2,10,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.684547105405,1.0,1_1,0.010101010101,1.0
-1,1.2,11,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.684547105405,1.0,1_1,0.010101010101,1.0
-1,1.2,12,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.684547105405,1.0,1_1,0.010101010101,1.0
-1,1.3,10,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.728968627466,1.0,1_1,0.010101010101,1.0
-1,1.3,11,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.728968627466,1.0,1_1,0.010101010101,1.0
-1,1.3,12,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.728968627466,1.0,1_1,0.010101010101,1.0
-1,1.4,10,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.770513243301,1.0,1_1,0.010101010101,1.0
-1,1.4,11,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.770513243301,1.0,1_1,0.010101010101,1.0
-1,1.4,12,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.770513243301,1.0,1_1,0.010101010101,1.0
-1,1.5,10,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.809016994529,1.0,1_1,0.010101010101,1.0
-1,1.5,11,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.809016994529,1.0,1_1,0.010101010101,1.0
-1,1.5,12,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.809016994529,1.0,1_1,0.010101010101,1.0
-1,1.6,10,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.844327926,1.0,1_1,0.010101010101,1.0
-1,1.6,11,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.844327926,1.0,1_1,0.010101010101,1.0
-1,1.6,12,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.844327926,1.0,1_1,0.010101010101,1.0
-1,1.7,10,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.876306679651,1.0,1_1,0.010101010101,1.0
-1,1.7,11,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.876306679651,1.0,1_1,0.010101010101,1.0
-1,1.7,12,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.876306679651,1.0,1_1,0.010101010101,1.0
-1,1.8,10,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.904827051854,1.0,1_1,0.010101010101,1.0
-1,1.8,11,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.904827051854,1.0,1_1,0.010101010101,1.0
-1,1.8,12,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.904827051854,1.0,1_1,0.010101010101,1.0
-1,1.9,10,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.929776486118,1.0,1_1,0.010101010101,1.0
-1,1.9,11,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.929776486118,1.0,1_1,0.010101010101,1.0
-1,1.9,12,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.929776486118,1.0,1_1,0.010101010101,1.0
-1,2.0,10,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.951056515685,1.0,1_1,0.010101010101,1.0
-1,2.0,11,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.951056515685,1.0,1_1,0.010101010101,1.0
-1,2.0,12,0,1.0,63.808135,-63.808135,31.904067,-2.0,-29.904067,0.951056515685,1.0,1_1,0.010101010101,1.0
+RAVEN_sample_ID,Time,Year,_ROM_Cluster,scaling,Dispatch__steamer__production__steam,Dispatch__generator__production__electricity,Dispatch__generator__production__steam,Dispatch__electr_market__production__electricity,Dispatch__electr_flex__production__electricity,Signal,ProbabilityWeight-steamer_capacity,prefix,ProbabilityWeight,PointProbability
+0,0.0,10,0,1.0,63.808135,31.904067,-63.808135,-2.0,-29.904067,-2.51201283814e-10,1.0,1_0,1.0,0.010101010101
+0,0.0,11,0,1.0,63.808135,31.904067,-63.808135,-2.0,-29.904067,-2.51201283814e-10,1.0,1_0,1.0,0.010101010101
+0,0.0,12,0,1.0,63.808135,31.904067,-63.808135,-2.0,-29.904067,-2.51201283814e-10,1.0,1_0,1.0,0.010101010101
+0,0.1,10,0,1.0,63.808135,31.904067,-63.808135,-2.0,-29.904067,0.0627905200127,1.0,1_0,1.0,0.010101010101
+0,0.1,11,0,1.0,63.808135,31.904067,-63.808135,-2.0,-29.904067,0.0627905200127,1.0,1_0,1.0,0.010101010101
+0,0.1,12,0,1.0,63.808135,31.904067,-63.808135,-2.0,-29.904067,0.0627905200127,1.0,1_0,1.0,0.010101010101
+0,0.2,10,0,1.0,63.808135,31.904067,-63.808135,-2.0,-29.904067,0.125333234008,1.0,1_0,1.0,0.010101010101
+0,0.2,11,0,1.0,63.808135,31.904067,-63.808135,-2.0,-29.904067,0.125333234008,1.0,1_0,1.0,0.010101010101
+0,0.2,12,0,1.0,63.808135,31.904067,-63.808135,-2.0,-29.904067,0.125333234008,1.0,1_0,1.0,0.010101010101
+0,0.3,10,0,1.0,63.808135,31.904067,-63.808135,-2.0,-29.904067,0.187381314977,1.0,1_0,1.0,0.010101010101
+0,0.3,11,0,1.0,63.808135,31.904067,-63.808135,-2.0,-29.904067,0.187381314977,1.0,1_0,1.0,0.010101010101
+0,0.3,12,0,1.0,63.808135,31.904067,-63.808135,-2.0,-29.904067,0.187381314977,1.0,1_0,1.0,0.010101010101
+0,0.4,10,0,1.0,63.808135,31.904067,-63.808135,-2.0,-29.904067,0.248689886838,1.0,1_0,1.0,0.010101010101
+0,0.4,11,0,1.0,63.808135,31.904067,-63.808135,-2.0,-29.904067,0.248689886838,1.0,1_0,1.0,0.010101010101
+0,0.4,12,0,1.0,63.808135,31.904067,-63.808135,-2.0,-29.904067,0.248689886838,1.0,1_0,1.0,0.010101010101
+0,0.5,10,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.309016994721,1.0,1_0,1.0,0.010101010101
+0,0.5,11,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.309016994721,1.0,1_0,1.0,0.010101010101
+0,0.5,12,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.309016994721,1.0,1_0,1.0,0.010101010101
+0,0.6,10,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.368124552404,1.0,1_0,1.0,0.010101010101
+0,0.6,11,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.368124552404,1.0,1_0,1.0,0.010101010101
+0,0.6,12,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.368124552404,1.0,1_0,1.0,0.010101010101
+0,0.7,10,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.42577929116,1.0,1_0,1.0,0.010101010101
+0,0.7,11,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.42577929116,1.0,1_0,1.0,0.010101010101
+0,0.7,12,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.42577929116,1.0,1_0,1.0,0.010101010101
+0,0.8,10,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.481753674692,1.0,1_0,1.0,0.010101010101
+0,0.8,11,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.481753674692,1.0,1_0,1.0,0.010101010101
+0,0.8,12,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.481753674692,1.0,1_0,1.0,0.010101010101
+0,0.9,10,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.535826794514,1.0,1_0,1.0,0.010101010101
+0,0.9,11,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.535826794514,1.0,1_0,1.0,0.010101010101
+0,0.9,12,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.535826794514,1.0,1_0,1.0,0.010101010101
+0,1.0,10,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.587785252891,1.0,1_0,1.0,0.010101010101
+0,1.0,11,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.587785252891,1.0,1_0,1.0,0.010101010101
+0,1.0,12,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.587785252891,1.0,1_0,1.0,0.010101010101
+0,1.1,10,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.637423990329,1.0,1_0,1.0,0.010101010101
+0,1.1,11,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.637423990329,1.0,1_0,1.0,0.010101010101
+0,1.1,12,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.637423990329,1.0,1_0,1.0,0.010101010101
+0,1.2,10,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.684547106263,1.0,1_0,1.0,0.010101010101
+0,1.2,11,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.684547106263,1.0,1_0,1.0,0.010101010101
+0,1.2,12,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.684547106263,1.0,1_0,1.0,0.010101010101
+0,1.3,10,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.728968626879,1.0,1_0,1.0,0.010101010101
+0,1.3,11,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.728968626879,1.0,1_0,1.0,0.010101010101
+0,1.3,12,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.728968626879,1.0,1_0,1.0,0.010101010101
+0,1.4,10,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.770513243013,1.0,1_0,1.0,0.010101010101
+0,1.4,11,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.770513243013,1.0,1_0,1.0,0.010101010101
+0,1.4,12,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.770513243013,1.0,1_0,1.0,0.010101010101
+0,1.5,10,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.809016994001,1.0,1_0,1.0,0.010101010101
+0,1.5,11,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.809016994001,1.0,1_0,1.0,0.010101010101
+0,1.5,12,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.809016994001,1.0,1_0,1.0,0.010101010101
+0,1.6,10,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.844327925978,1.0,1_0,1.0,0.010101010101
+0,1.6,11,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.844327925978,1.0,1_0,1.0,0.010101010101
+0,1.6,12,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.844327925978,1.0,1_0,1.0,0.010101010101
+0,1.7,10,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.87630668031,1.0,1_0,1.0,0.010101010101
+0,1.7,11,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.87630668031,1.0,1_0,1.0,0.010101010101
+0,1.7,12,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.87630668031,1.0,1_0,1.0,0.010101010101
+0,1.8,10,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.904827051898,1.0,1_0,1.0,0.010101010101
+0,1.8,11,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.904827051898,1.0,1_0,1.0,0.010101010101
+0,1.8,12,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.904827051898,1.0,1_0,1.0,0.010101010101
+0,1.9,10,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.929776485406,1.0,1_0,1.0,0.010101010101
+0,1.9,11,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.929776485406,1.0,1_0,1.0,0.010101010101
+0,1.9,12,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.929776485406,1.0,1_0,1.0,0.010101010101
+0,2.0,10,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.951056516745,1.0,1_0,1.0,0.010101010101
+0,2.0,11,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.951056516745,1.0,1_0,1.0,0.010101010101
+0,2.0,12,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.951056516745,1.0,1_0,1.0,0.010101010101
+1,0.0,10,0,1.0,63.808135,31.904067,-63.808135,-2.0,-29.904067,8.34672934618e-10,1.0,1_1,1.0,0.010101010101
+1,0.0,11,0,1.0,63.808135,31.904067,-63.808135,-2.0,-29.904067,8.34672934618e-10,1.0,1_1,1.0,0.010101010101
+1,0.0,12,0,1.0,63.808135,31.904067,-63.808135,-2.0,-29.904067,8.34672934618e-10,1.0,1_1,1.0,0.010101010101
+1,0.1,10,0,1.0,63.808135,31.904067,-63.808135,-2.0,-29.904067,0.0627905194607,1.0,1_1,1.0,0.010101010101
+1,0.1,11,0,1.0,63.808135,31.904067,-63.808135,-2.0,-29.904067,0.0627905194607,1.0,1_1,1.0,0.010101010101
+1,0.1,12,0,1.0,63.808135,31.904067,-63.808135,-2.0,-29.904067,0.0627905194607,1.0,1_1,1.0,0.010101010101
+1,0.2,10,0,1.0,63.808135,31.904067,-63.808135,-2.0,-29.904067,0.125333233727,1.0,1_1,1.0,0.010101010101
+1,0.2,11,0,1.0,63.808135,31.904067,-63.808135,-2.0,-29.904067,0.125333233727,1.0,1_1,1.0,0.010101010101
+1,0.2,12,0,1.0,63.808135,31.904067,-63.808135,-2.0,-29.904067,0.125333233727,1.0,1_1,1.0,0.010101010101
+1,0.3,10,0,1.0,63.808135,31.904067,-63.808135,-2.0,-29.904067,0.187381314472,1.0,1_1,1.0,0.010101010101
+1,0.3,11,0,1.0,63.808135,31.904067,-63.808135,-2.0,-29.904067,0.187381314472,1.0,1_1,1.0,0.010101010101
+1,0.3,12,0,1.0,63.808135,31.904067,-63.808135,-2.0,-29.904067,0.187381314472,1.0,1_1,1.0,0.010101010101
+1,0.4,10,0,1.0,63.808135,31.904067,-63.808135,-2.0,-29.904067,0.248689887657,1.0,1_1,1.0,0.010101010101
+1,0.4,11,0,1.0,63.808135,31.904067,-63.808135,-2.0,-29.904067,0.248689887657,1.0,1_1,1.0,0.010101010101
+1,0.4,12,0,1.0,63.808135,31.904067,-63.808135,-2.0,-29.904067,0.248689887657,1.0,1_1,1.0,0.010101010101
+1,0.5,10,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.309016994562,1.0,1_1,1.0,0.010101010101
+1,0.5,11,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.309016994562,1.0,1_1,1.0,0.010101010101
+1,0.5,12,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.309016994562,1.0,1_1,1.0,0.010101010101
+1,0.6,10,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.368124553372,1.0,1_1,1.0,0.010101010101
+1,0.6,11,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.368124553372,1.0,1_1,1.0,0.010101010101
+1,0.6,12,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.368124553372,1.0,1_1,1.0,0.010101010101
+1,0.7,10,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.425779291511,1.0,1_1,1.0,0.010101010101
+1,0.7,11,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.425779291511,1.0,1_1,1.0,0.010101010101
+1,0.7,12,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.425779291511,1.0,1_1,1.0,0.010101010101
+1,0.8,10,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.481753674744,1.0,1_1,1.0,0.010101010101
+1,0.8,11,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.481753674744,1.0,1_1,1.0,0.010101010101
+1,0.8,12,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.481753674744,1.0,1_1,1.0,0.010101010101
+1,0.9,10,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.535826794514,1.0,1_1,1.0,0.010101010101
+1,0.9,11,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.535826794514,1.0,1_1,1.0,0.010101010101
+1,0.9,12,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.535826794514,1.0,1_1,1.0,0.010101010101
+1,1.0,10,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.587785251807,1.0,1_1,1.0,0.010101010101
+1,1.0,11,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.587785251807,1.0,1_1,1.0,0.010101010101
+1,1.0,12,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.587785251807,1.0,1_1,1.0,0.010101010101
+1,1.1,10,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.637423989248,1.0,1_1,1.0,0.010101010101
+1,1.1,11,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.637423989248,1.0,1_1,1.0,0.010101010101
+1,1.1,12,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.637423989248,1.0,1_1,1.0,0.010101010101
+1,1.2,10,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.684547105405,1.0,1_1,1.0,0.010101010101
+1,1.2,11,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.684547105405,1.0,1_1,1.0,0.010101010101
+1,1.2,12,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.684547105405,1.0,1_1,1.0,0.010101010101
+1,1.3,10,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.728968627466,1.0,1_1,1.0,0.010101010101
+1,1.3,11,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.728968627466,1.0,1_1,1.0,0.010101010101
+1,1.3,12,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.728968627466,1.0,1_1,1.0,0.010101010101
+1,1.4,10,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.770513243301,1.0,1_1,1.0,0.010101010101
+1,1.4,11,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.770513243301,1.0,1_1,1.0,0.010101010101
+1,1.4,12,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.770513243301,1.0,1_1,1.0,0.010101010101
+1,1.5,10,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.809016994529,1.0,1_1,1.0,0.010101010101
+1,1.5,11,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.809016994529,1.0,1_1,1.0,0.010101010101
+1,1.5,12,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.809016994529,1.0,1_1,1.0,0.010101010101
+1,1.6,10,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.844327926,1.0,1_1,1.0,0.010101010101
+1,1.6,11,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.844327926,1.0,1_1,1.0,0.010101010101
+1,1.6,12,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.844327926,1.0,1_1,1.0,0.010101010101
+1,1.7,10,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.876306679651,1.0,1_1,1.0,0.010101010101
+1,1.7,11,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.876306679651,1.0,1_1,1.0,0.010101010101
+1,1.7,12,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.876306679651,1.0,1_1,1.0,0.010101010101
+1,1.8,10,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.904827051854,1.0,1_1,1.0,0.010101010101
+1,1.8,11,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.904827051854,1.0,1_1,1.0,0.010101010101
+1,1.8,12,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.904827051854,1.0,1_1,1.0,0.010101010101
+1,1.9,10,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.929776486118,1.0,1_1,1.0,0.010101010101
+1,1.9,11,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.929776486118,1.0,1_1,1.0,0.010101010101
+1,1.9,12,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.929776486118,1.0,1_1,1.0,0.010101010101
+1,2.0,10,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.951056515685,1.0,1_1,1.0,0.010101010101
+1,2.0,11,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.951056515685,1.0,1_1,1.0,0.010101010101
+1,2.0,12,0,1.0,63.808135,31.904067,-63.808135,0.0,-31.904067,0.951056515685,1.0,1_1,1.0,0.010101010101
diff --git a/tests/integration_tests/mechanics/debug_mode/heron_input.xml b/tests/integration_tests/mechanics/debug_mode/heron_input.xml
index 07d97923..7d4a0206 100644
--- a/tests/integration_tests/mechanics/debug_mode/heron_input.xml
+++ b/tests/integration_tests/mechanics/debug_mode/heron_input.xml
@@ -75,8 +75,7 @@
         <lifetime>30</lifetime>
         <CashFlow name="e_sales" type="repeating" taxable='True' inflation='none' mult_target='False'>
           <driver>
-            <Function method="electric_consume">transfers</Function>
-            <multiplier>-1</multiplier>
+            <activity>electricity</activity>
           </driver>
           <reference_price>
             <fixed_value>0.5</fixed_value>
@@ -95,8 +94,7 @@
         <lifetime>30</lifetime>
         <CashFlow name="e_sales" type="repeating" taxable='True' inflation='none' mult_target='False'>
           <driver>
-            <Function method="electric_consume">transfers</Function>
-            <multiplier>-1</multiplier>
+            <activity>electricity</activity>
           </driver>
           <reference_price>
             <Function method="flex_price">transfers</Function>
diff --git a/tests/integration_tests/mechanics/debug_mode/transfers.py b/tests/integration_tests/mechanics/debug_mode/transfers.py
index cbb0bbad..85b349ef 100644
--- a/tests/integration_tests/mechanics/debug_mode/transfers.py
+++ b/tests/integration_tests/mechanics/debug_mode/transfers.py
@@ -5,21 +5,6 @@
   Implements transfer functions
 """
 
-def electric_consume(data, meta):
-  """
-    Provides the amount of electricity consumed.
-    @ In, data, dict, request for data
-    @ In, meta, dict, state information
-    @ Out, data, dict, filled data
-    @ In, meta, dict, state information
-  """
-  activity = meta['HERON']['activity']
-  # TODO a get_activity method for the dispatcher -> returns object-safe activity (expression or value)?
-  amount = activity['electricity']
-  # NOTE multiplier is -1 in the input!
-  data = {'driver': amount}
-  return data, meta
-
 def flex_price(data, meta):
   """
     Determines the price of electricity.
diff --git a/tests/integration_tests/mechanics/labels/heron_input.xml b/tests/integration_tests/mechanics/labels/heron_input.xml
index 62a2557c..355f2f48 100644
--- a/tests/integration_tests/mechanics/labels/heron_input.xml
+++ b/tests/integration_tests/mechanics/labels/heron_input.xml
@@ -82,8 +82,7 @@
         <lifetime>30</lifetime>
         <CashFlow name="e_sales" type="repeating" taxable='True' inflation='none' mult_target='False'>
           <driver>
-            <Function method="electric_consume">transfers</Function>
-            <multiplier>-1</multiplier>
+            <activity>electricity</activity>
           </driver>
           <reference_price>
             <fixed_value>0.5</fixed_value>
@@ -92,13 +91,6 @@
       </economics>
     </Component>
 
-    <!--
-         design notes
-         add a <resource> node for at least CashFlow drivers
-         add multiplier and absolute value options for manipulating entries
-         maybe disallow <linear> and <resource> except for applicable ValuedParams?
-    -->
-
     <Component name="electr_flex">
       <demands resource="electricity" dispatch="dependent">
         <capacity>
@@ -109,8 +101,7 @@
         <lifetime>30</lifetime>
         <CashFlow name="e_sales" type="repeating" taxable='True' inflation='none' mult_target='False'>
           <driver>
-            <Function method="electric_consume">transfers</Function>
-            <multiplier>-1</multiplier>
+            <activity>electricity</activity>
           </driver>
           <reference_price>
             <Function method="flex_price">transfers</Function>
diff --git a/tests/integration_tests/mechanics/labels/transfers.py b/tests/integration_tests/mechanics/labels/transfers.py
index 701e692b..f1365c1f 100644
--- a/tests/integration_tests/mechanics/labels/transfers.py
+++ b/tests/integration_tests/mechanics/labels/transfers.py
@@ -5,13 +5,6 @@
   Implements transfer functions
 """
 
-def electric_consume(data, meta):
-  # works with pyomo
-  activity = meta['HERON']['activity']
-  amount = -1 * activity['electricity']
-  data = {'driver': amount}
-  return data, meta,
-
 def flex_price(data, meta):
   sine = meta['HERON']['RAVEN_vars']['Signal']
   t = meta['HERON']['time_index']
diff --git a/tests/integration_tests/mechanics/min_demand/transfers.py b/tests/integration_tests/mechanics/min_demand/transfers.py
index 5aea03a9..e58b2fa8 100644
--- a/tests/integration_tests/mechanics/min_demand/transfers.py
+++ b/tests/integration_tests/mechanics/min_demand/transfers.py
@@ -13,7 +13,7 @@ def electric_source(data, meta):
     @ Out, meta, dict, state information
   """
   # flip sign because we consume the electricity
-  E = -1.0 * meta['HERON']['activity']['electricity']
+  E = -1.0 * meta['HERON']['activity']['production']['electricity']
   data = {'driver': E}
   return data, meta
 
diff --git a/tests/integration_tests/mechanics/multimarket_fix_price/cash.xml b/tests/integration_tests/mechanics/multimarket_fix_price/cash.xml
deleted file mode 100644
index 9c4bae2e..00000000
--- a/tests/integration_tests/mechanics/multimarket_fix_price/cash.xml
+++ /dev/null
@@ -1,35 +0,0 @@
-<Economics verbosity="50">
-  <Global>
-    <DiscountRate>0.08</DiscountRate>
-    <tax>0.0</tax>
-    <inflation>0.0</inflation>
-    <Indicator name="NPV" target="0">Electric_Grid_1|e_sales
-      Electric_Grid_2|e_sales</Indicator>
-    <ProjectTime>3.0</ProjectTime>
-  </Global>
-
-  <Component name="Generator">
-    <Life_time>27</Life_time>
-    <CashFlows />
-  </Component>
-
-  <Component name="Electric_Grid_1">
-    <Life_time>3</Life_time>
-    <CashFlows>
-      <Recurring inflation="none" mult_target="False" name="e_sales" tax="True">
-        <driver>_rec_hour_Function_Electric_Grid_1_e_sales</driver>
-        <alpha>-1.0</alpha>
-      </Recurring>
-    </CashFlows>
-  </Component>
-
-  <Component name="Electric_Grid_2">
-    <Life_time>3</Life_time>
-    <CashFlows>
-      <Recurring inflation="none" mult_target="False" name="e_sales" tax="True">
-        <driver>_rec_hour_Function_Electric_Grid_2_e_sales</driver>
-        <alpha>-1.0</alpha>
-      </Recurring>
-    </CashFlows>
-  </Component>
-</Economics>
diff --git a/tests/integration_tests/mechanics/multimarket_fix_price/heron_input.xml b/tests/integration_tests/mechanics/multimarket_fix_price/heron_input.xml
index fad98e60..ef45f3a1 100644
--- a/tests/integration_tests/mechanics/multimarket_fix_price/heron_input.xml
+++ b/tests/integration_tests/mechanics/multimarket_fix_price/heron_input.xml
@@ -58,7 +58,8 @@
         <lifetime>3</lifetime>
         <CashFlow name="e_sales" type="repeating" taxable='True' inflation='none' mult_target='False'>
           <driver>
-            <Function method="electric_source">transfers</Function>
+            <activity>electricity</activity>
+            <multiplier>-1</multiplier>
           </driver>
           <reference_price>
             <fixed_value>10.0</fixed_value>
@@ -83,7 +84,8 @@
         <lifetime>3</lifetime>
         <CashFlow name="e_sales" type="repeating" taxable='True' inflation='none' mult_target='False'>
           <driver>
-            <Function method="electric_source">transfers</Function>
+            <activity>electricity</activity>
+            <multiplier>-1</multiplier>
           </driver>
           <reference_price>
             <fixed_value>0.1</fixed_value>
diff --git a/tests/integration_tests/mechanics/multimarket_fix_price/transfers.py b/tests/integration_tests/mechanics/multimarket_fix_price/transfers.py
index 5be0eadc..e665736a 100644
--- a/tests/integration_tests/mechanics/multimarket_fix_price/transfers.py
+++ b/tests/integration_tests/mechanics/multimarket_fix_price/transfers.py
@@ -5,15 +5,6 @@
   Implements transfer functions
 """
 
-def electric_source(data, meta):
-  """
-    Fix up electricity for how much the Grids consume
-  """
-  # flip sign because we consume the electricity
-  E = -1.0 * meta['HERON']['activity']['electricity']
-  data = {'driver': E}
-  return data, meta
-
 def power_conversion(data, meta):
   """
     How to get power from the incoming signal
diff --git a/tests/integration_tests/mechanics/multirun_sweep_opt/functions.py b/tests/integration_tests/mechanics/multirun_sweep_opt/functions.py
index 32fe8c35..900b0e8d 100644
--- a/tests/integration_tests/mechanics/multirun_sweep_opt/functions.py
+++ b/tests/integration_tests/mechanics/multirun_sweep_opt/functions.py
@@ -16,15 +16,3 @@ def capacity(data, meta):
   data = {'driver': c}
   return data, meta
 
-def activity(data, meta):
-  """
-    return usage of resource "a"
-    @ In, data, dict, data requeset
-    @ In, meta, dict, state information
-    @ Out, data, dict, filled data
-    @ Out, meta, dict, state information
-  """
-  a = meta['HERON']['activity']['a']
-  data = {'driver': a}
-  return data, meta
-
diff --git a/tests/integration_tests/mechanics/multirun_sweep_opt/heron_input_opt.xml b/tests/integration_tests/mechanics/multirun_sweep_opt/heron_input_opt.xml
index 3941ec11..5f73b79b 100644
--- a/tests/integration_tests/mechanics/multirun_sweep_opt/heron_input_opt.xml
+++ b/tests/integration_tests/mechanics/multirun_sweep_opt/heron_input_opt.xml
@@ -71,7 +71,7 @@
           </CashFlow>
           <CashFlow name="VOM" type="repeating" taxable="False" inflation="none" mult_target="False">
             <driver>
-              <Function method="activity">functions</Function>
+              <activity>a</activity>
             </driver>
             <reference_price>
               <fixed_value>-1</fixed_value>
@@ -90,7 +90,7 @@
           <lifetime>30</lifetime>
           <CashFlow name="sales" type="repeating" taxable="False" inflation="none" mult_target="False">
             <driver>
-              <Function method="activity">functions</Function>
+              <activity>a</activity>
             </driver>
             <reference_price>
               <fixed_value>10.0</fixed_value>
diff --git a/tests/integration_tests/mechanics/multirun_sweep_opt/heron_input_sweep.xml b/tests/integration_tests/mechanics/multirun_sweep_opt/heron_input_sweep.xml
index 966bc9f8..1000cedc 100644
--- a/tests/integration_tests/mechanics/multirun_sweep_opt/heron_input_sweep.xml
+++ b/tests/integration_tests/mechanics/multirun_sweep_opt/heron_input_sweep.xml
@@ -71,7 +71,7 @@
           </CashFlow>
           <CashFlow name="VOM" type="repeating" taxable="False" inflation="none" mult_target="False">
             <driver>
-              <Function method="activity">functions</Function>
+              <activity>a</activity>
             </driver>
             <reference_price>
               <fixed_value>-1</fixed_value>
@@ -90,7 +90,7 @@
           <lifetime>30</lifetime>
           <CashFlow name="sales" type="repeating" taxable="False" inflation="none" mult_target="False">
             <driver>
-              <Function method="activity">functions</Function>
+              <activity>a</activity>
             </driver>
             <reference_price>
               <fixed_value>10.0</fixed_value>
diff --git a/tests/integration_tests/mechanics/pyomo_options/heron_input.xml b/tests/integration_tests/mechanics/pyomo_options/heron_input.xml
index a7889c48..a088f8d1 100644
--- a/tests/integration_tests/mechanics/pyomo_options/heron_input.xml
+++ b/tests/integration_tests/mechanics/pyomo_options/heron_input.xml
@@ -31,7 +31,7 @@
       <pyomo>
         <rolling_window_length>8</rolling_window_length>
         <debug_mode>True</debug_mode>
-        <solver>ipopt</solver>
+        <solver>cbc</solver>
       </pyomo>
     </dispatcher>
   </Case>
@@ -90,8 +90,8 @@
         <lifetime>30</lifetime>
         <CashFlow name="e_sales" type="repeating" taxable='True' inflation='none' mult_target='False'>
           <driver>
-            <Function method="electric_consume">transfers</Function>
-            <!-- <resource multiplier="-1" absolute="True">electricity</resource> -->
+            <activity>electricity</activity>
+            <multiplier>-1</multiplier>
           </driver>
           <reference_price>
             <fixed_value>0.5</fixed_value>
@@ -111,7 +111,8 @@
         <lifetime>30</lifetime>
         <CashFlow name="e_sales" type="repeating" taxable='True' inflation='none' mult_target='False'>
           <driver>
-            <Function method="electric_consume">transfers</Function>
+            <activity>electricity</activity>
+            <multiplier>-1</multiplier>
           </driver>
           <reference_price>
             <Function method="flex_price">transfers</Function>
diff --git a/tests/integration_tests/mechanics/pyomo_options/transfers.py b/tests/integration_tests/mechanics/pyomo_options/transfers.py
index 24bcd582..666313e8 100644
--- a/tests/integration_tests/mechanics/pyomo_options/transfers.py
+++ b/tests/integration_tests/mechanics/pyomo_options/transfers.py
@@ -5,19 +5,6 @@
   Implements transfer functions
 """
 
-def electric_consume(data, meta):
-  """
-    Swaps the sign for electricity consumption to yield positive cash values.
-    @ In, data, dict, information to be filled before return
-    @ In, meta, dict, additional information from HERON state
-    @ Out, data, dict, information filled
-    @ Out, meta, dict, additional information from HERON state
-  """
-  activity = meta['HERON']['activity']
-  amount = -1 * activity['electricity']
-  data = {'driver': amount}
-  return data, meta
-
 def flex_price(data, meta):
   """
     Gathers and modifies the ARMA signal to produce a price history ranging
diff --git a/tests/integration_tests/mechanics/storage_func/heron_input.xml b/tests/integration_tests/mechanics/storage_func/heron_input.xml
index 981197a9..4cd64f19 100644
--- a/tests/integration_tests/mechanics/storage_func/heron_input.xml
+++ b/tests/integration_tests/mechanics/storage_func/heron_input.xml
@@ -88,8 +88,8 @@
         <lifetime>30</lifetime>
         <CashFlow name="e_sales" type="repeating" taxable='True' inflation='none' mult_target='False'>
           <driver>
-            <Function method="electric_consume">transfers</Function>
-            <!-- <resource multiplier="-1" absolute="True">electricity</resource> -->
+            <activity>electricity</activity>
+            <multiplier>-1</multiplier>
           </driver>
           <reference_price>
             <fixed_value>0.5</fixed_value>
@@ -108,7 +108,8 @@
         <lifetime>30</lifetime>
         <CashFlow name="e_sales" type="repeating" taxable='True' inflation='none' mult_target='False'>
           <driver>
-            <Function method="electric_consume">transfers</Function>
+            <activity>electricity</activity>
+            <multiplier>-1</multiplier>
           </driver>
           <reference_price>
             <Function method="flex_price">transfers</Function>
diff --git a/tests/integration_tests/mechanics/storage_func/run_storage.py b/tests/integration_tests/mechanics/storage_func/run_storage.py
index 65d7a6e4..0188a4be 100644
--- a/tests/integration_tests/mechanics/storage_func/run_storage.py
+++ b/tests/integration_tests/mechanics/storage_func/run_storage.py
@@ -23,17 +23,17 @@ def tiered(data, meta):
   capacity = comp.get_capacity(meta)[0]['steam']
   generator = [meta['HERON']['Components'][c] for c, comp in enumerate(meta['HERON']['Components']) if comp.name == 'generator'][0]
   gen_cap = generator.get_capacity(meta)[0]['steam'] * dt * -1
-  level = comp.get_interaction().get_initial_level(meta)
-  activity = np.zeros(len(time))
+  current_level = comp.get_interaction().get_initial_level(meta)
+  levels = np.zeros(len(time))
   for t, _ in enumerate(time):
     if price[t] > 0.7:
       # empty storage and sell it all, up to generator capacity
-      release = min(level, gen_cap - steam_produced)
-      level -= release
+      release = min(current_level, gen_cap - steam_produced)
+      current_level -= release
     elif price[t] < 0.3:
       # take and store all the electricity we can
-      absorb = min(steam_produced, capacity - level)
-      level += absorb
-    activity[t] = level
-  data['activity'] = activity
+      absorb = min(steam_produced, capacity - current_level)
+      current_level += absorb
+    levels[t] = current_level
+  data['level'] = levels
   return data, meta
diff --git a/tests/integration_tests/mechanics/storage_func/transfers.py b/tests/integration_tests/mechanics/storage_func/transfers.py
index 24bcd582..666313e8 100644
--- a/tests/integration_tests/mechanics/storage_func/transfers.py
+++ b/tests/integration_tests/mechanics/storage_func/transfers.py
@@ -5,19 +5,6 @@
   Implements transfer functions
 """
 
-def electric_consume(data, meta):
-  """
-    Swaps the sign for electricity consumption to yield positive cash values.
-    @ In, data, dict, information to be filled before return
-    @ In, meta, dict, additional information from HERON state
-    @ Out, data, dict, information filled
-    @ Out, meta, dict, additional information from HERON state
-  """
-  activity = meta['HERON']['activity']
-  amount = -1 * activity['electricity']
-  data = {'driver': amount}
-  return data, meta
-
 def flex_price(data, meta):
   """
     Gathers and modifies the ARMA signal to produce a price history ranging
diff --git a/tests/integration_tests/mechanics/validator/heron_input.xml b/tests/integration_tests/mechanics/validator/heron_input.xml
index b80d124b..abe3daef 100644
--- a/tests/integration_tests/mechanics/validator/heron_input.xml
+++ b/tests/integration_tests/mechanics/validator/heron_input.xml
@@ -75,8 +75,8 @@
         <lifetime>30</lifetime>
         <CashFlow name="e_sales" type="repeating" taxable='True' inflation='none' mult_target='False'>
           <driver>
-            <Function method="electric_consume">transfers</Function>
-            <!-- <resource multiplier="-1" absolute="True">electricity</resource> -->
+            <activity>electricity</activity>
+            <multiplier>-1</multiplier>
           </driver>
           <reference_price>
             <fixed_value>0.5</fixed_value>
@@ -95,7 +95,8 @@
         <lifetime>30</lifetime>
         <CashFlow name="e_sales" type="repeating" taxable='True' inflation='none' mult_target='False'>
           <driver>
-            <Function method="electric_consume">transfers</Function>
+            <activity>electricity</activity>
+            <multiplier>-1</multiplier>
           </driver>
           <reference_price>
             <Function method="flex_price">transfers</Function>
diff --git a/tests/integration_tests/mechanics/validator/transfers.py b/tests/integration_tests/mechanics/validator/transfers.py
index 164fc613..9202a3d8 100644
--- a/tests/integration_tests/mechanics/validator/transfers.py
+++ b/tests/integration_tests/mechanics/validator/transfers.py
@@ -5,19 +5,6 @@
   Implements transfer functions
 """
 
-def electric_consume(data, meta):
-  """
-    Transfer function to change sign of electricity consumed, for cashflows
-    @ In, data, dict, partial requested activity
-    @ In, meta, dict, additional info
-    @ Out, data, dict, filled-in requested activity
-    @ Out, meta, dict, additional info (possibly modified, possibly not)
-  """
-  activity = meta['HERON']['activity']
-  amount = -1 * activity['electricity']
-  data = {'driver': amount}
-  return data, meta
-
 def flex_price(data, meta):
   """
     Transfer function to acquire the flexible price of electricity
diff --git a/tests/integration_tests/mechanics/var_demand_fix_price/cash.xml b/tests/integration_tests/mechanics/var_demand_fix_price/cash.xml
deleted file mode 100644
index eb9d4775..00000000
--- a/tests/integration_tests/mechanics/var_demand_fix_price/cash.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<Economics verbosity="50">
-  <Global>
-    <DiscountRate>0.08</DiscountRate>
-    <tax>0.0</tax>
-    <inflation>0.0</inflation>
-    <Indicator name="NPV" target="0">Electric_Grid|e_sales</Indicator>
-    <ProjectTime>2.0</ProjectTime>
-  </Global>
-
-  <Component name="BOP">
-    <Life_time>27</Life_time>
-    <CashFlows />
-  </Component>
-
-  <Component name="Electric_Grid">
-    <Life_time>3</Life_time>
-    <CashFlows>
-      <Recurring inflation="none" mult_target="False" name="e_sales" tax="True">
-        <driver>_rec_hour_Function_Electric_Grid_e_sales</driver>
-        <alpha>-1.0</alpha>
-      </Recurring>
-    </CashFlows>
-  </Component>
-</Economics>
diff --git a/tests/integration_tests/mechanics/var_demand_fix_price/heron_input.xml b/tests/integration_tests/mechanics/var_demand_fix_price/heron_input.xml
index f6776be1..5a98f023 100644
--- a/tests/integration_tests/mechanics/var_demand_fix_price/heron_input.xml
+++ b/tests/integration_tests/mechanics/var_demand_fix_price/heron_input.xml
@@ -58,7 +58,8 @@
         <lifetime>3</lifetime>
         <CashFlow name="e_sales" type="repeating" taxable='True' inflation='none' mult_target='False'>
           <driver>
-		        <Function method="electric_source">transfer</Function>
+            <activity>electricity</activity>
+            <multiplier>-1</multiplier>
           </driver>
           <reference_price>
             <fixed_value>0.1</fixed_value>
diff --git a/tests/integration_tests/mechanics/var_demand_fix_price/transfers.py b/tests/integration_tests/mechanics/var_demand_fix_price/transfers.py
index 5be0eadc..e665736a 100644
--- a/tests/integration_tests/mechanics/var_demand_fix_price/transfers.py
+++ b/tests/integration_tests/mechanics/var_demand_fix_price/transfers.py
@@ -5,15 +5,6 @@
   Implements transfer functions
 """
 
-def electric_source(data, meta):
-  """
-    Fix up electricity for how much the Grids consume
-  """
-  # flip sign because we consume the electricity
-  E = -1.0 * meta['HERON']['activity']['electricity']
-  data = {'driver': E}
-  return data, meta
-
 def power_conversion(data, meta):
   """
     How to get power from the incoming signal
diff --git a/tests/integration_tests/mechanics/var_demand_var_price/heron_input.xml b/tests/integration_tests/mechanics/var_demand_var_price/heron_input.xml
index 2d1547a7..1f78a950 100644
--- a/tests/integration_tests/mechanics/var_demand_var_price/heron_input.xml
+++ b/tests/integration_tests/mechanics/var_demand_var_price/heron_input.xml
@@ -58,7 +58,8 @@
         <lifetime>3</lifetime>
         <CashFlow name="e_sales" type="repeating" taxable='True' inflation='none' mult_target='False'>
           <driver>
-            <Function method="electric_source">transfers</Function>
+            <activity>electricity</activity>
+            <multiplier>-1</multiplier>
           </driver>
           <reference_price>
             <ARMA variable="Signal">Speed</ARMA>
diff --git a/tests/integration_tests/mechanics/var_demand_var_price/transfers.py b/tests/integration_tests/mechanics/var_demand_var_price/transfers.py
index 5be0eadc..e665736a 100644
--- a/tests/integration_tests/mechanics/var_demand_var_price/transfers.py
+++ b/tests/integration_tests/mechanics/var_demand_var_price/transfers.py
@@ -5,15 +5,6 @@
   Implements transfer functions
 """
 
-def electric_source(data, meta):
-  """
-    Fix up electricity for how much the Grids consume
-  """
-  # flip sign because we consume the electricity
-  E = -1.0 * meta['HERON']['activity']['electricity']
-  data = {'driver': E}
-  return data, meta
-
 def power_conversion(data, meta):
   """
     How to get power from the incoming signal
diff --git a/tests/integration_tests/workflows/production_flex/transfers.py b/tests/integration_tests/workflows/production_flex/transfers.py
index cbb0bbad..039a469e 100644
--- a/tests/integration_tests/workflows/production_flex/transfers.py
+++ b/tests/integration_tests/workflows/production_flex/transfers.py
@@ -15,7 +15,7 @@ def electric_consume(data, meta):
   """
   activity = meta['HERON']['activity']
   # TODO a get_activity method for the dispatcher -> returns object-safe activity (expression or value)?
-  amount = activity['electricity']
+  amount = activity['production']['electricity']
   # NOTE multiplier is -1 in the input!
   data = {'driver': amount}
   return data, meta
diff --git a/tests/integration_tests/workflows/storage/analytic.xlsx b/tests/integration_tests/workflows/storage/analytic.xlsx
index 5011f352..89c5b079 100644
Binary files a/tests/integration_tests/workflows/storage/analytic.xlsx and b/tests/integration_tests/workflows/storage/analytic.xlsx differ
diff --git a/tests/integration_tests/workflows/storage/gold/Sweep_Runs_o/sweep.csv b/tests/integration_tests/workflows/storage/gold/Sweep_Runs_o/sweep.csv
index 1a44d86a..7df99e16 100644
--- a/tests/integration_tests/workflows/storage/gold/Sweep_Runs_o/sweep.csv
+++ b/tests/integration_tests/workflows/storage/gold/Sweep_Runs_o/sweep.csv
@@ -1,3 +1,3 @@
-steamer_capacity,steam_storage_capacity,generator_capacity,electr_market_capacity,electr_flex_capacity,mean_NPV,std_NPV,med_NPV,max_NPV,min_NPV,perc_5_NPV,perc_95_NPV,samp_NPV,var_NPV,prefix,PointProbability,ProbabilityWeight,ProbabilityWeight-steamer_capacity
-1.0,100.0,-99.0,-2.0,-1e+200,28.0280044015,1.63813697062e-08,28.0280043909,28.0280044203,28.0280043909,28.0280043909,28.0280043932,3.0,2.68349273452e-16,1,0.010101010101,0.5,0.5
-100.0,100.0,-99.0,-2.0,-1e+200,595.529477388,4.2779215532e-07,595.529476912,595.529477741,595.529476912,595.529476912,595.529477511,3.0,1.83006128153e-13,2,0.010101010101,0.5,0.5
+steamer_capacity,steam_storage_capacity,generator_capacity,electr_market_capacity,electr_flex_capacity,mean_NPV,std_NPV,med_NPV,max_NPV,min_NPV,perc_5_NPV,perc_95_NPV,samp_NPV,var_NPV,ProbabilityWeight,ProbabilityWeight-steamer_capacity,PointProbability,prefix
+1.0,100.0,-99.0,-2.0,-1e+200,27.3667638372,1.56343784157e-08,27.3667638291,27.3667638552,27.3667638272,27.3667638272,27.3667638552,3.0,2.44433788446e-16,0.5,0.5,0.010101010101,1
+100.0,100.0,-99.0,-2.0,-1e+200,621.494308544,3.98747544645e-07,621.494308515,621.494308957,621.494308161,621.494308161,621.494308957,3.0,1.5899960436e-13,0.5,0.5,0.010101010101,2
diff --git a/tests/integration_tests/workflows/storage/heron_input.xml b/tests/integration_tests/workflows/storage/heron_input.xml
index 9aca19dd..46ba6010 100644
--- a/tests/integration_tests/workflows/storage/heron_input.xml
+++ b/tests/integration_tests/workflows/storage/heron_input.xml
@@ -26,7 +26,7 @@
       <verbosity>50</verbosity>
     </economics>
     <dispatcher>
-      <pyomo/>
+      <pyomo><debug_mode>True</debug_mode></pyomo>
     </dispatcher>
   </Case>
 
@@ -50,6 +50,7 @@
         <initial_stored>
           <fixed_value>1</fixed_value>
         </initial_stored>
+        <RTE>0.9</RTE>
       </stores>
       <economics>
         <lifetime>10</lifetime>
@@ -84,8 +85,8 @@
         <lifetime>30</lifetime>
         <CashFlow name="e_sales" type="repeating" taxable='True' inflation='none' mult_target='False'>
           <driver>
-            <Function method="electric_consume">transfers</Function>
-            <!-- <resource multiplier="-1" absolute="True">electricity</resource> -->
+            <activity>electricity</activity>
+            <multiplier>-1</multiplier>
           </driver>
           <reference_price>
             <fixed_value>0.5</fixed_value>
@@ -105,7 +106,8 @@
         <lifetime>30</lifetime>
         <CashFlow name="e_sales" type="repeating" taxable='True' inflation='none' mult_target='False'>
           <driver>
-            <Function method="electric_consume">transfers</Function>
+            <activity>electricity</activity>
+            <multiplier>-1</multiplier>
           </driver>
           <reference_price>
             <Function method="flex_price">transfers</Function>
diff --git a/tests/integration_tests/workflows/storage/transfers.py b/tests/integration_tests/workflows/storage/transfers.py
index 24bcd582..666313e8 100644
--- a/tests/integration_tests/workflows/storage/transfers.py
+++ b/tests/integration_tests/workflows/storage/transfers.py
@@ -5,19 +5,6 @@
   Implements transfer functions
 """
 
-def electric_consume(data, meta):
-  """
-    Swaps the sign for electricity consumption to yield positive cash values.
-    @ In, data, dict, information to be filled before return
-    @ In, meta, dict, additional information from HERON state
-    @ Out, data, dict, information filled
-    @ Out, meta, dict, additional information from HERON state
-  """
-  activity = meta['HERON']['activity']
-  amount = -1 * activity['electricity']
-  data = {'driver': amount}
-  return data, meta
-
 def flex_price(data, meta):
   """
     Gathers and modifies the ARMA signal to produce a price history ranging