Skip to content

Commit

Permalink
New approach to handling plugin configuration.
Browse files Browse the repository at this point in the history
* You give each plugin an alias. You use a line of code like `config.plugin.my_alias = 'my_package.plugin'`. This assigns the alias "my_alias" to the plugin, and imports the specified python module. The module can be specified as a python module name, as an actual module object, or as the path to a python file.

* Setting up the plugin alias as described above will give the plugin a chance to supply any configuration settings it uses. The plugin-specific configuration is stored in config.plugin.my_alias.

* You can set up a plugin on the command line (or a config file) using --plugin=alias:module, where alias is the alias that determines where the plugin's configuration is stored, and module is either the module name or the module file path. The plugin may register new command line options, which are made available immediately. Plugins may choose to incorporate the alias into their option names.

* You can also set up a plugin using a dictionary. The alias is put into the dictionary with the key "module".  Other plugin configuation values can be put in the same dictionary.

* Plugins must supply a register_plugins method that registers whatever callbacks it uses. This method called just before the simulation starts.

Here are a couple examples of how you might set up a plugin:

  import my_plugins.pricing
  config.plugin.pricing = my_plugins.pricing
  config.plugin.pricing.option1 = 100
  config.plugin.pricing.option2 = 'fast'

  config.plugin.stats_writer = '../plugins/stats.py'
  config.plugin.stats_writer.write_hourly = True

  options = {'plugin':
                {'ruc_monitor':
                  { 'module': 'my_plugins.ruc',
                    'verbose':True
                  }
                }
             }
  config.set_value(options)
darrylmelander authored and bknueven committed Aug 23, 2021
1 parent e3e546b commit f3969a7
Showing 8 changed files with 150 additions and 111 deletions.
36 changes: 22 additions & 14 deletions prescient/plugins/plugin_registration.py
Original file line number Diff line number Diff line change
@@ -11,7 +11,14 @@
from pyomo.common.fileutils import import_file

class PluginRegistrationContext:
''' Object that plugins use to register their callbacks
''' Object that plugins and other code use to register their callbacks.
This class provides a registration-centric view of a PluginCallbackManager.
Despite its name, it can be used by more than just plugins. Plugins modules
must supply a function called register_plugins; its implementation should
register appropriate callbacks on the PluginRegistrationContext provided as
an argument to that function. Non-plugin code that wishes to receive callbacks
should call the appropriate registration functions as part of simulation setup.
'''

def __init__(self):
@@ -20,21 +27,22 @@ def __init__(self):

def register_plugin(
self,
path: str,
config: PrescientConfig) -> None:
''' Call the plugin registration method in the specified file
plugin_module:'module',
config: PrescientConfig,
plugin_config: ConfigDict) -> None:
''' Allow a plugin module to register for callbacks
This method calls the module's register_plugins method, passing
this registration context as an argument. The register_plugins
method should use the context to register whatever callbacks are
appropriate for the plugin. The method is also provided with the
PrescientConfig to be used in the simulation, as well as the
plugin-specific portion of the configuration.
'''
try:
plugin_module = import_file(path)
except:
import os
print(os.getcwd())
raise RuntimeError(f"Could not locate plugin module={path}")

register_func = getattr(plugin_module, "register_plugins", None)
if register_func is None:
raise RuntimeError(f"plugin module={path} does not have a required method, register_plugins")
register_func(self, config)
raise RuntimeError(f"plugin module={plugin_module} does not have a required method, register_plugins")
register_func(self, config, plugin_config)

def register_for_hourly_stats(
self,
@@ -109,7 +117,7 @@ def register_after_ruc_generation_callback(
self,
callback: Callable[[Options, Simulator, RucPlan, str, int], None]
) -> None:
''' Register a callback to be called after each new RUC pair is generated.
''' Register a callback to be called after each new RUC plan is generated.
The callback is called after both the forecast and actuals RUCs have been
generated, just before they are stored in the DataManager.
135 changes: 79 additions & 56 deletions prescient/simulator/config.py
Original file line number Diff line number Diff line change
@@ -25,13 +25,16 @@
from pyomo.common.config import (ConfigDict,
ConfigValue,
ConfigList,
DynamicImplicitDomain,
Module,
In,
InEnum,
PositiveInt,
NonNegativeInt,
PositiveFloat,
NonNegativeFloat,
Path,
MarkImmutable
)

from prescient.plugins import PluginRegistrationContext
@@ -54,14 +57,48 @@ def __init__(self):

self.plugin_context = PluginRegistrationContext()

def register_plugin(key, value):
''' Handle intial plugin setup
Arguments
---------
key - str
The alias for this plugin in the configuration
value - str, module, or dict
If a string, the name of the python module or the python file for this plugin.
If a module, the plugin's python module.
If a dict, the initial values for any properties listed in the dict. One of the
dict's keys MUST be 'module', and must be either a module, a string identifying
the module, or a string identifying the module's *.py file.
'''
# Defaults, if value is not a dict
mod_spec=value
init_values = {}

# Override defaults if value is a dict
if isinstance(value, dict):
if 'module' not in value:
raise RuntimeError(f"Attempt to register '{key}' plugin without a module attribute")
mod_spec=value['module']
init_values = value.copy()
del(init_values['module'])

domain = Module()
module = domain(mod_spec)

c = module.get_configuration(key)
c.declare('module', ConfigValue(module, domain=domain))
MarkImmutable(c.get('module'))
c.set_value(init_values)
return c

# We put this first so that plugins will be registered before any other
# options are applied, which lets them add custom command line options
# before they are potentially used.
self.declare("plugin", ConfigList(
domain=_PluginPath(self),
default=[],
description="The path of a python module that extends prescient behavior",
)).declare_as_argument()
self.declare("plugin", ConfigDict(
implicit=True,
implicit_domain=DynamicImplicitDomain(register_plugin),
description="Settings for python modules that extends prescient behavior",
))

self.declare("start_date", ConfigValue(
domain=_StartDate,
@@ -365,31 +402,16 @@ def parse_args(self, args: List[str]) -> ConfigDict:
self.import_argparse(args)
return self

def set_value(self, value, skip_implicit=False):
if value is None:
return self
if (type(value) is not dict) and \
(not isinstance(value, ConfigDict)):
raise ValueError("Expected dict value for %s.set_value, found %s" %
(self.name(True), type(value).__name__))

if 'plugin' in value:
self.plugin.append(value['plugin'])
value = {**value}
del(value['plugin'])
super().set_value(value)



def _construct_options_parser(config: PrescientConfig) -> ArgumentParser:
'''
Make a new parser that can parse standard and custom command line options.
Custom options are provided by plugin modules. Plugins are specified
as command-line arguments "--plugin=<module name>", where <module name>
refers to a python module. The plugin may provide new command line
options by calling "prescient.plugins.add_custom_commandline_option"
when loaded.
as command-line arguments "--plugin=<alias>:<module name>", where <alias>
is the name the plugin will be known as in the configuration, and
<module name> identifies a python module by name or by path. Any configuration
items defined by the plugin will be available at config.plugin.alias.
'''

# To support the ability to add new command line options to the line that
@@ -402,60 +424,61 @@ def _construct_options_parser(config: PrescientConfig) -> ArgumentParser:
parser = ArgumentParser()
parser._inner_parse = parser.parse_args

def split_plugin_spec(spec:str) -> (str, str):
''' Return the plugin's alias and module from the plugin name/path
'''
result = spec.split(':', 1)
if len(result) == 1:
raise ValueError("No alias found in plugin specification. Correct format: <alias>:<module_path_or_name>")
return result

def outer_parse(args=None, values=None):
if args is None:
args = sys.argv[1:]

# Manually check each argument against --plugin=<module>,
# Manually check each argument against --plugin=<alias>:<module>,
# give plugins a chance to install their options.
stand_alone_opt = '--plugin'
prefix = "--plugin="
next_arg_is_module = False
# When a plugin is imported, its
# plugin behaviors are registered.
found_plugin=False
for arg in args:
if arg.startswith(prefix):
module_name = arg[len(prefix):]
config.plugin_context.register_plugin(module_name, config)
if next_arg_is_module:
module_spec = arg
alias, mod = split_plugin_spec(module_spec)
config.plugin[alias] = mod
next_arg_is_module = False
found_plugin=True
elif arg.startswith(prefix):
module_spec = arg[len(prefix):]
alias, mod = split_plugin_spec(module_spec)
config.plugin[alias] = mod
found_plugin=True
elif arg == stand_alone_opt:
next_arg_is_module = True
elif next_arg_is_module:
module_name = arg
config.plugin_context.register_plugin(module_name, config)
next_arg_is_module = False


# load the arguments into the ArgumentParser
config.initialize_argparse(parser)

# Remove plugins from args so they don't get re-handled
i = 0
while (i < len(args)):
if args[i].startswith(prefix):
del(args[i])
elif args[i] == stand_alone_opt:
del(args[i])
del(args[i])
else:
i += 1
if found_plugin:
args = args.copy()
i = 0
while (i < len(args)):
if args[i].startswith(prefix):
del(args[i])
elif args[i] == stand_alone_opt:
del(args[i])
del(args[i])
else:
i += 1

# Now parse for real, with any new options in place.
return parser._inner_parse(args, values)

parser.parse_args = outer_parse
return parser

class _PluginPath(Path):
''' A Path that registers a plugin when its path is set
'''
def __init__(self, parent_config: PrescientConfig):
self.config = parent_config
super().__init__()

def __call__(self, data):
path = super().__call__(data)
self.config.plugin_context.register_plugin(path, self.config)
return path

class _InEnumStr(InEnum):
''' A bit more forgiving string to enum parser
'''
14 changes: 7 additions & 7 deletions prescient/simulator/oracle_manager.py
Original file line number Diff line number Diff line change
@@ -126,7 +126,7 @@ def call_initialization_oracle(self, options: Options, time_step: PrescientTime)
self.data_manager.set_pending_ruc_plan(options, ruc_plan)
self.data_manager.activate_pending_ruc(options)

self.simulator.plugin_manager.invoke_after_ruc_activation_callbacks(options, self.simulator)
self.simulator.callback_manager.invoke_after_ruc_activation_callbacks(options, self.simulator)
return ruc_plan

def call_planning_oracle(self, options: Options, time_step: PrescientTime):
@@ -154,7 +154,7 @@ def _generate_ruc(self, options, uc_date, uc_hour, sim_state_for_ruc):
options.run_ruc_with_next_day_data,
)

self.simulator.plugin_manager.invoke_before_ruc_solve_callbacks(options, self.simulator, deterministic_ruc_instance, uc_date, uc_hour)
self.simulator.callback_manager.invoke_before_ruc_solve_callbacks(options, self.simulator, deterministic_ruc_instance, uc_date, uc_hour)

deterministic_ruc_instance = self.engine.solve_deterministic_ruc(
options,
@@ -184,12 +184,12 @@ def _generate_ruc(self, options, uc_date, uc_hour, sim_state_for_ruc):
)

result = RucPlan(simulation_actuals, deterministic_ruc_instance, ruc_market)
self.simulator.plugin_manager.invoke_after_ruc_generation_callbacks(options, self.simulator, result, uc_date, uc_hour)
self.simulator.callback_manager.invoke_after_ruc_generation_callbacks(options, self.simulator, result, uc_date, uc_hour)
return result

def activate_pending_ruc(self, options: Options):
self.data_manager.activate_pending_ruc(options)
self.simulator.plugin_manager.invoke_after_ruc_activation_callbacks(options, self.simulator)
self.simulator.callback_manager.invoke_after_ruc_activation_callbacks(options, self.simulator)

def call_operation_oracle(self, options: Options, time_step: PrescientTime):
# determine the SCED execution mode, in terms of how discrepancies between forecast and actuals are handled.
@@ -219,7 +219,7 @@ def call_operation_oracle(self, options: Options, time_step: PrescientTime):
lp_filename=lp_filename
)

self.simulator.plugin_manager.invoke_before_operations_solve_callbacks(options, self.simulator, current_sced_instance)
self.simulator.callback_manager.invoke_before_operations_solve_callbacks(options, self.simulator, current_sced_instance)

current_sced_instance, solve_time = self.engine.solve_sced_instance(options, current_sced_instance,
options.output_sced_initial_conditions,
@@ -249,15 +249,15 @@ def call_operation_oracle(self, options: Options, time_step: PrescientTime):
lmp_sced = self.engine.create_and_solve_lmp(options, current_sced_instance)

self.data_manager.apply_sced(options, current_sced_instance)
self.simulator.plugin_manager.invoke_after_operations_callbacks(options, self.simulator, current_sced_instance)
self.simulator.callback_manager.invoke_after_operations_callbacks(options, self.simulator, current_sced_instance)

ops_stats = self.simulator.stats_manager.collect_operations(current_sced_instance,
solve_time,
lmp_sced,
pre_quickstart_cache,
self.engine.operations_data_extractor)

self.simulator.plugin_manager.invoke_update_operations_stats_callbacks(options, self.simulator, ops_stats)
self.simulator.callback_manager.invoke_update_operations_stats_callbacks(options, self.simulator, ops_stats)
self._report_sced_stats(ops_stats)

if options.compute_market_settlements:
6 changes: 1 addition & 5 deletions prescient/simulator/prescient.py
Original file line number Diff line number Diff line change
@@ -13,9 +13,6 @@

import sys

import prescient.plugins.internal
import prescient.simulator.config

from .config import PrescientConfig
from .options import Options
from .simulator import Simulator
@@ -43,8 +40,7 @@ def __init__(self):
self.simulate_called = False

super().__init__(engine, time_manager, data_manager, oracle_manager,
stats_manager, reporting_manager,
self.config.plugin_context.callback_manager)
stats_manager, reporting_manager)

def simulate(self, **options):
if 'config_file' in options:
17 changes: 11 additions & 6 deletions prescient/simulator/simulator.py
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@

from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .internal import PluginCallbackManager
from prescient.plugins.internal import PluginCallbackManager
from .reporting_manager import ReportingManager

import os
@@ -31,8 +31,7 @@ def __init__(self, model_engine: ModelingEngine,
data_manager: DataManager,
oracle_manager: OracleManager,
stats_manager: StatsManager,
reporting_manager: ReportingManager,
plugin_manager: PluginCallbackManager
reporting_manager: ReportingManager
):

print("Initializing simulation...")
@@ -63,7 +62,6 @@ def __init__(self, model_engine: ModelingEngine,
self.oracle_manager = oracle_manager
self.stats_manager = stats_manager
self.reporting_manager = reporting_manager
self.plugin_manager = plugin_manager


def simulate(self, options):
@@ -74,8 +72,15 @@ def simulate(self, options):
oracle_manager = self.oracle_manager
stats_manager = self.stats_manager
reporting_manager = self.reporting_manager
# This comes from the config rather than being created here
# because non-plugins may have registered for callbacks too.
self.callback_manager = options.plugin_context.callback_manager

self.plugin_manager.invoke_options_preview_callbacks(options)
for plugin_config in options.plugin.values():
plugin_module = plugin_config.module
options.plugin_context.register_plugin(plugin_module, options, plugin_config)

self.callback_manager.invoke_options_preview_callbacks(options)

engine.initialize(options)
time_manager.initialize(options)
@@ -84,7 +89,7 @@ def simulate(self, options):
stats_manager.initialize(options)
reporting_manager.initialize(options, stats_manager)

self.plugin_manager.invoke_initialization_callbacks(options, self)
self.callback_manager.invoke_initialization_callbacks(options, self)

first_time_step = time_manager.get_first_time_step()
oracle_manager.call_initialization_oracle(options, first_time_step)
4 changes: 2 additions & 2 deletions tests/simulator_tests/test_cases/simulate_deterministic.txt
Original file line number Diff line number Diff line change
@@ -15,5 +15,5 @@ command/exec simulator.py
--ruc-horizon=36
--enforce-sced-shutdown-ramprate
--no-startup-shutdown-curves
--plugin=test_plugin.py
--print-callback-message
--plugin=test:test_plugin.py
--test.print-callback-message
43 changes: 24 additions & 19 deletions tests/simulator_tests/test_cases/test_plugin.py
Original file line number Diff line number Diff line change
@@ -5,65 +5,70 @@
from prescient.simulator.config import PrescientConfig
import prescient.plugins as pplugins

from pyomo.common.config import ConfigValue
from pyomo.common.config import ConfigDict, ConfigValue

# This is a required function, must have this name and signature
def get_configuration(key):
config = ConfigDict()
config.declare('print_callback_message',
ConfigValue(domain=bool,
description='Print a message when callback is called',
default=False)).declare_as_argument(f'--{key}.print-callback-message')
return config

def msg(callback_name, options=None):
if options is None or options.print_callback_message:
print(f"Called plugin function {callback_name}")

# This is a required function, must have this name and signature
def register_plugins(context: pplugins.PluginRegistrationContext,
config: PrescientConfig) -> None:

config.declare('print_callback_message',
ConfigValue(domain=bool,
description='Print a message when callback is called',
default=False)).declare_as_argument()
def register_plugins(context: pplugins.PluginRegistrationContext,
options: PrescientConfig,
plugin_config: ConfigDict) -> None:

def hourly_stats_callback(hourly_stats):
msg('hourly_stats_callback')
msg('hourly_stats_callback', plugin_config)
context.register_for_hourly_stats(hourly_stats_callback)

def daily_stats_callback(daily_stats):
msg('daily_stats_callback')
msg('daily_stats_callback', plugin_config)
context.register_for_daily_stats(daily_stats_callback)

def overall_stats_callback(overall_stats):
msg('overall_stats_callback')
msg('overall_stats_callback', plugin_config)
context.register_for_overall_stats(overall_stats_callback)

def options_preview_callback(options):
msg('options_preview_callback', options)
msg('options_preview_callback', plugin_config)
context.register_options_preview_callback(options_preview_callback)

def initialization_callback(options, simulator):
msg('initialization_callback', options)
msg('initialization_callback', plugin_config)
context.register_initialization_callback(initialization_callback)

def finalization_callback(options, simulator):
msg('finalization_callback', plugin_config)
context.register_finalization_callback(finalization_callback)

def before_ruc_solve_callback(options, simulator, ruc_model, uc_date, uc_hour):
msg('before_ruc_solve_callback', options)
msg('before_ruc_solve_callback', plugin_config)
context.register_before_ruc_solve_callback(before_ruc_solve_callback)

def after_ruc_generation_callback(options, simulator, ruc_plan, uc_date, uc_hour):
msg('after_ruc_generation_callback', options)
msg('after_ruc_generation_callback', plugin_config)
context.register_after_ruc_generation_callback(after_ruc_generation_callback)

def after_ruc_activation_callback(options, simulator):
msg('after_ruc_activation_callback', options)
msg('after_ruc_activation_callback', plugin_config)
context.register_after_ruc_activation_callback(after_ruc_activation_callback)

def before_operations_solve_callback(options, simulator, sced_model):
msg('before_operations_solve_callback', options)
msg('before_operations_solve_callback', plugin_config)
context.register_before_operations_solve_callback(before_operations_solve_callback)

def after_operations_callback(options, simulator, sced_model):
msg('after_operations_callback', options)
msg('after_operations_callback', plugin_config)
context.register_after_operations_callback(after_operations_callback)

def update_operations_stats_callback(options, simulator, operations_stats):
msg('update_operations_stats_callback', options)
msg('update_operations_stats_callback', plugin_config)
context.register_update_operations_stats_callback(update_operations_stats_callback)
6 changes: 4 additions & 2 deletions tests/simulator_tests/test_sim_rts_mod.py
Original file line number Diff line number Diff line change
@@ -59,6 +59,8 @@ def _run_simulator(self):

simulator_config_filename = self.simulator_config_filename
script, options = runner.parse_commands(simulator_config_filename)
# Consider using the following instead of launching a separate process:
# Prescient().simulate(config_file=simulator_config_filename)

if sys.platform.startswith('win'):
subprocess.call([script] + options, shell=True)
@@ -176,8 +178,8 @@ def _run_simulator(self):
options = {**base_options}
options['data_directory'] = 'deterministic_scenarios'
options['output_directory'] = 'deterministic_simulation_output_python'
options['plugin'] = 'test_plugin.py'
options['print_callback_message'] = True
options['plugin'] = {'test':{'module':'test_plugin.py',
'print_callback_message':True}}
Prescient().simulate(**options)

# test options are correctly re-freshed, Python, and network

0 comments on commit f3969a7

Please sign in to comment.