From 31d4dc71584b01f612970a0122342fd9c3f1277b Mon Sep 17 00:00:00 2001 From: Jacob Bryan Date: Wed, 6 Nov 2024 13:12:13 -0700 Subject: [PATCH 01/54] flat RAVEN for static history --- src/DispatchManager.py | 61 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/src/DispatchManager.py b/src/DispatchManager.py index 43fcf60d..b93eb9ae 100644 --- a/src/DispatchManager.py +++ b/src/DispatchManager.py @@ -901,4 +901,63 @@ def run(self, raven, raven_dict): dispatch, metrics, tot_activity = runner.run(raven_vars) runner.save_variables(raven, dispatch, metrics, tot_activity) - + def createNewInput(self, raven, myInput, samplerType, **kwargs): + """ + This function will return a new input to be submitted to the model, it is called by the sampler. + @ In, raven, ravenframework.utils.utils.Object, externalizable RAVEN object + @ In, myInput, list, the inputs (list) to start from to generate the new one + @ In, samplerType, string, is the type of sampler that is calling to generate a new input + @ In, **kwargs, dict, is a dictionary that contains the information coming from the sampler, + a mandatory key is the sampledVars'that contains a dictionary {'name variable':value} + @ Out, newInput, dict, new input dict for the HERON dispatcher + """ + # newInput dict needs to include info on sampled variables. + newInput = {k: np.atleast_1d(v) for k, v in kwargs.get("SampledVars", {}).items()} + + # In the case of static histories, the history information might not be provided through a sampler + # and instead be found in a DataObject in the myInput list. If that's the case, we need to add all + # indices and signals from the DataObject into the newInput dict as if they were sampled values. + # We assert here that there must be EXACTLY ONE element in myInput. That one data object must be + # either named "dispatch_placeholder" or is treated as having a static history signal. + if len(myInput) != 1: + raise TypeError("HERON.DispatchManager model must receive exactly 1 DataObject as an input! " + f"Received: {len(myInput)} inputs of types {[type(inp) for inp in myInput]}.") + + data_obj = myInput[0] + # If the data object is just the dispatch placeholder, there's nothing else we need to add. + # FIXME: Can we handle this in a way that doesn't rely on naming conventions? Note that the + # dispatch_placeholder object isn't necessarily required, so we DON'T want to throw + # an error is dispatch_placeholder isn't present. + if data_obj.name == "dispatch_placeholder": + return newInput + + # Otherwise, we assume the data object must hold a static history data set. We add this to the + # sampled variables. + print("A StaticHistory has been provided! Adding to sampled variables.") + # TODO: Support more flexible inputs for static histories. Any reason to not allow users to use a + # PointSet or a HistorySet to specify a static history? + if data_obj.__class__.__name__ != "DataSet": + raise TypeError("The fixed time series data set (StaticHistory) must be provided as a DataSet. " + f"Received: {type(data_obj)}.") + + # Get index and variable names from the DataSet + data, meta = data_obj.getData() + indexes = list(data.indexes.keys()) + variables = set(data.variables.keys()) - set(indexes) + indexes.remove("RAVEN_sample_ID") # FIXME: CSV source for DataSet must include RAVEN_sample_ID, but this + # isn't a useful index for a single history. Will cause issues if we + # don't get rid of it here, but it's ugly to have to include this. + + # Add the DataObject indexes and variables to newInput + extracted_values = {k: np.squeeze(np.atleast_1d(data.variables.get(k))) for k in set(indexes) | variables} + newInput.update(extracted_values) + + # Create an _indexMap for the extracted values. RAVEN makes this as a dict wrapped in a numpy array for + # some reason, so we need to mimic that here. + index_map = np.array([{var_name: indexes for var_name in variables}]) # FIXME: Fails if indexes list is in a different order than expected. Can we make this robust to list order? + # NOTE: The cause of the index name list order issue comes from the DispatchRunner._slice_signals method implementation, which looks at the index of the macro and time step index names + # to slice the signal. If the order of the index names is wrong in the index map, the _slice_signals method slices along the wrong axis. We'll leave this be for now, but a more robust + # solution to time history slicing would be useful. + newInput["_indexMap"] = index_map + + return newInput From 949da7f14c75b886eb4ba7ed415198b275140c8f Mon Sep 17 00:00:00 2001 From: Jacob Bryan Date: Mon, 11 Nov 2024 16:22:05 -0700 Subject: [PATCH 02/54] feature driver approach to template driver refactor --- src/Cases.py | 21 ++- templates/debug_outer.xml | 0 templates/feature_drivers/__init__.py | 8 + templates/feature_drivers/debug.py | 21 +++ templates/feature_drivers/feature_driver.py | 115 ++++++++++++ templates/feature_drivers/histories.py | 9 + templates/feature_drivers/inputs.py | 1 + templates/feature_drivers/labels.py | 21 +++ templates/feature_drivers/models.py | 110 ++++++++++++ templates/feature_drivers/multiruns.py | 0 templates/feature_drivers/naming_templates.py | 3 + templates/feature_drivers/optimizers.py | 65 +++++++ templates/feature_drivers/plots.py | 71 ++++++++ templates/feature_drivers/samplers.py | 55 ++++++ templates/feature_drivers/snippets.py | 48 +++++ templates/feature_drivers/utils.py | 52 ++++++ templates/feature_drivers/vargroups.py | 23 +++ templates/flat.xml | 0 templates/inner_static.xml | 121 +++++++++++++ templates/inner_static_hists.xml | 137 +++++++++++++++ templates/inner_synth.xml | 121 +++++++++++++ templates/inner_synth_hists.xml | 121 +++++++++++++ templates/outer_debug.xml | 105 +++++++++++ templates/outer_opt.xml | 166 ++++++++++++++++++ templates/outer_sweep.xml | 63 +++++++ 25 files changed, 1446 insertions(+), 11 deletions(-) create mode 100644 templates/debug_outer.xml create mode 100644 templates/feature_drivers/__init__.py create mode 100644 templates/feature_drivers/debug.py create mode 100644 templates/feature_drivers/feature_driver.py create mode 100644 templates/feature_drivers/histories.py create mode 100644 templates/feature_drivers/inputs.py create mode 100644 templates/feature_drivers/labels.py create mode 100644 templates/feature_drivers/models.py create mode 100644 templates/feature_drivers/multiruns.py create mode 100644 templates/feature_drivers/naming_templates.py create mode 100644 templates/feature_drivers/optimizers.py create mode 100644 templates/feature_drivers/plots.py create mode 100644 templates/feature_drivers/samplers.py create mode 100644 templates/feature_drivers/snippets.py create mode 100644 templates/feature_drivers/utils.py create mode 100644 templates/feature_drivers/vargroups.py create mode 100644 templates/flat.xml create mode 100644 templates/inner_static.xml create mode 100644 templates/inner_static_hists.xml create mode 100644 templates/inner_synth.xml create mode 100644 templates/inner_synth_hists.xml create mode 100644 templates/outer_debug.xml create mode 100644 templates/outer_opt.xml create mode 100644 templates/outer_sweep.xml diff --git a/src/Cases.py b/src/Cases.py index dbe38e43..adac039c 100644 --- a/src/Cases.py +++ b/src/Cases.py @@ -1362,26 +1362,25 @@ def write_workflows(self, components, sources, loc): @ Out, None """ # load templates - template_class = self._load_template() - inner, outer = template_class.createWorkflow(self, components, sources) - - template_class.writeWorkflow((inner, outer), loc) + driver = self._load_template(components, sources) + driver.create_workflows(self, components, sources) + driver.write_workflows(loc) #### UTILITIES #### - def _load_template(self): + def _load_template(self, components, sources): """ Loads template files for modification - @ In, None - @ Out, template_class, RAVEN Template, instantiated Template class + @ In, components, HERON components, components for the simulation + @ In, sources, HERON sources, sources for the simulation + @ Out, template_class, TemplateDriver, instantiated TemplateDriver class """ src_dir = os.path.dirname(os.path.realpath(__file__)) heron_dir = os.path.abspath(os.path.join(src_dir, '..')) template_dir = os.path.abspath(os.path.join(heron_dir, 'templates')) - template_name = 'template_driver' + template_name = 'template_drivers' # import template module sys.path.append(heron_dir) module = importlib.import_module(f'templates.{template_name}', package="HERON") # load template, perform actions - template_class = module.Template(messageHandler=self.messageHandler) - template_class.loadTemplate(template_dir) - return template_class + driver = module.create_template_driver(self, components, sources) + return driver diff --git a/templates/debug_outer.xml b/templates/debug_outer.xml new file mode 100644 index 00000000..e69de29b diff --git a/templates/feature_drivers/__init__.py b/templates/feature_drivers/__init__.py new file mode 100644 index 00000000..38da24eb --- /dev/null +++ b/templates/feature_drivers/__init__.py @@ -0,0 +1,8 @@ +from .feature_driver import FeatureDriver +from .samplers import * +from .optimizers import * +from .histories import * +from .models import * +from .multiruns import * +from .plots import * +from .vargroups import * diff --git a/templates/feature_drivers/debug.py b/templates/feature_drivers/debug.py new file mode 100644 index 00000000..ff16db6d --- /dev/null +++ b/templates/feature_drivers/debug.py @@ -0,0 +1,21 @@ +from feature_driver import FeatureCollection +from plots import HeronDispatchPlot, TealCashFlowPlot + +class DebugPlots(FeatureCollection): + """ + Debug mode plots + """ + def edit_template(self, template, case, components, sources): + """ + Edits the template in-place + @ In, template + @ In, case + @ In, components + @ In, sources + """ + if case.debug["dispatch_plot"]: + self._features.append(HeronDispatchPlot()) + if case.debug["cashflow_plot"]: + self._features.append(TealCashFlowPlot()) + + super().edit_template(template, case, components, sources) diff --git a/templates/feature_drivers/feature_driver.py b/templates/feature_drivers/feature_driver.py new file mode 100644 index 00000000..7b311af5 --- /dev/null +++ b/templates/feature_drivers/feature_driver.py @@ -0,0 +1,115 @@ +# Copyright 2020, Battelle Energy Alliance, LLC +# ALL RIGHTS RESERVED +""" + Drivers which translate HERON features to RAVEN workflow template changes. Add an option to HERON + by creating a new FeatureDriver for that feature. + + @author: Jacob Bryan (@j-bryan) + @date: 2024-11-08 +""" +import os +import shutil +from abc import abstractmethod +import xml.etree.ElementTree as ET + +from ..raven_templates import * + + +class FeatureDriver: + """ + FeatureDrivers translate HERON features to RAVEN workflow template changes. This makes feature + logic more portable across different templates. + """ + @property + @abstractmethod + def allowed_templates(self): + pass + + def edit_template(self, template, case, components, sources): + """ + Edits the template in-place + @ In, template + @ In, case + @ In, components + @ In, sources + """ + # The order of these functions is chosen based on which XML blocks reference others. Modifying the + # XML in this order should allow for editing nodes based on changes to objects they reference, if needed. + + # NOTE: FeatureDrivers which make simple edits don't necessarily need to follow this pattern, but this + # provides a general framework for making edits to RAVEN workflows. + + # Group 1 + self._modify_files(template, case, components, sources) + self._modify_databases(template, case, components, sources) + self._modify_distributions(template, case, components, sources) + self._modify_variablegroups(template, case, components, sources) + # Group 2 + self._modify_dataobjects(template, case, components, sources) # ref: VariableGroups + # Group 3 + self._modify_outstreams(template, case, components, sources) # ref: VariableGroups, DataObjects + self._modify_models(template, case, components, sources) # ref: DataObjects, other Models + self._modify_samplers(template, case, components, sources) # ref: Distributions, DataObjects + # Group 4 + self._modify_optimizers(template, case, components, sources) # ref: DataObjects, Models, Samplers + # Group 5 + self._modify_steps(template, case, components, sources) # ref: everything but VariableGroups and Distributions + # Group 6 + self._modify_runinfo(template, case, components, sources) # ref: Steps (step names) + + return template + + def _modify_files(self, template, case, components, sources): + pass + + def _modify_databases(self, template, case, components, sources): + pass + + def _modify_distributions(self, template, case, components, sources): + pass + + def _modify_variablegroups(self, template, case, components, sources): + pass + + def _modify_dataobjects(self, template, case, components, sources): + pass + + def _modify_outstreams(self, template, case, components, sources): + pass + + def _modify_models(self, template, case, components, sources): + pass + + def _modify_samplers(self, template, case, components, sources): + pass + + def _modify_optimizers(self, template, case, components, sources): + pass + + def _modify_steps(self, template, case, components, sources): + pass + + def _modify_runinfo(self, template, case, components, sources): + pass + +class FeatureCollection(FeatureDriver): + """ + Groups FeatureDrivers together to define more complex features. Useful for grouping together + features which are commonly used together, handling entity creation and entity settings + separately, and more! + """ + def __init__(self): + self._features = [] + + def edit_template(self, template, case, components, sources): + for feature in self._features: + feature.edit_template(template, case, components, sources) + + + + + +def subelement(parent, tag, attrib={}, text="", **extra): + new_element = ET.Element(tag, attrib, **extra) + new_element.text = text + parent.append(new_element) diff --git a/templates/feature_drivers/histories.py b/templates/feature_drivers/histories.py new file mode 100644 index 00000000..2f7fdc44 --- /dev/null +++ b/templates/feature_drivers/histories.py @@ -0,0 +1,9 @@ +from .feature_driver import FeatureDriver + + +class SyntheticHistory(FeatureDriver): + pass + + +class StaticHistory(FeatureDriver): + pass diff --git a/templates/feature_drivers/inputs.py b/templates/feature_drivers/inputs.py new file mode 100644 index 00000000..4287ca86 --- /dev/null +++ b/templates/feature_drivers/inputs.py @@ -0,0 +1 @@ +# \ No newline at end of file diff --git a/templates/feature_drivers/labels.py b/templates/feature_drivers/labels.py new file mode 100644 index 00000000..b9486cbd --- /dev/null +++ b/templates/feature_drivers/labels.py @@ -0,0 +1,21 @@ +import xml.etree.ElementTree as ET +from .feature_driver import FeatureDriver + + +# TODO: does it make sense to add this way? +class CaseLabels(FeatureDriver): + """ + Add case labels to sampler/optimizer node, + """ + def _modify_variablegroups(self, template, case, components, sources): + vargroups = template.find("VariableGroups") + gro_case_labels = ET.SubElement(vargroups, "Group", attrib={"name": "GRO_case_labels"}) + gro_case_labels.text = ', '.join([f'{key}_label' for key in case.get_labels().keys()]) + + def _modify_models(self, template, case, components, sources): + pass + + def _modify_samplers(self, template, case, components, sources): + for key, value in case.get_labels().items(): + var_name = self.namingTemplates['variable'].format(unit=key, feature='label') + samps_node.append(xmlUtils.newNode('constant', text=value, attrib={'name': var_name})) diff --git a/templates/feature_drivers/models.py b/templates/feature_drivers/models.py new file mode 100644 index 00000000..5c7d4bbe --- /dev/null +++ b/templates/feature_drivers/models.py @@ -0,0 +1,110 @@ +import os +import sys +import shutil +import xml.etree.ElementTree as ET + +from .feature_driver import FeatureDriver +from .snippets import EntityNode +from .utils import build_opt_metric_from_name, get_feature_list + +# load utils +sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..')) +import HERON.src._utils as hutils +sys.path.pop() + +# get raven location +RAVEN_LOC = os.path.abspath(os.path.join(hutils.get_raven_loc(), "ravenframework")) + + +class RavenCodeModel(FeatureDriver): + """ + Sets up a model to run inner RAVEN workflow + """ + def _modify_models(self, template, case, components, sources): + raven = template.find(".//Models/Code[@subType='RAVEN']") + raven_exec = raven.find('executable') + raven_exec_guess = os.path.abspath(os.path.join(RAVEN_LOC, '..', 'raven_framework')) + if os.path.exists(raven_exec_guess): + raven_exec.text = raven_exec_guess + elif shutil.which("raven_framework") is not None: + raven_exec.text = "raven_framework" + else: + raise RuntimeError("raven_framework not in PATH and not at "+raven_exec_guess) + # custom python command for running raven (for example, "coverage run") + if case.get_py_cmd_for_raven() is not None: + attribs = {'type': 'prepend', 'arg': case.get_py_cmd_for_raven()} + new = ET.Element('clargs', attrib=attribs) + raven.append(new) + # conversion script + conv = raven.find('conversion').find('input') + conv.attrib['source'] = '../write_inner.py' + + # Set variable aliases for Inner + alias_template = 'Samplers|MonteCarlo@name:mc_arma_dispatch|constant@name:{}' + for component in components: + name = component.name + attribs = {"variable": f"{name}_capacity", "type": "input"} + alias = ET.Element("alias", attribs) + alias.text = alias_template.format() + raven.append(alias) + + # # label aliases placed inside models + # for label in case.get_labels(): + # attribs = {'variable': f'{label}_label', 'type':'input'} + # new = xmlUtils.newNode('alias', text=text.format(label + '_label'), attrib=attribs) + # raven.append(new) + + # # data handling: inner to outer data format + # if case.data_handling['inner_to_outer'] == 'csv': + # # swap the outputDatabase to outputExportOutStreams + # output_node = template.find('Models').find('Code').find('outputDatabase') + # output_node.tag = 'outputExportOutStreams' + # # no need to change name, as database and outstream have the same name + + +class InnerDataHandling(FeatureDriver): + def _modify_models(self, template, case, components, sources): + # label aliases placed inside models + for label in case.get_labels(): + attribs = {'variable': f'{label}_label', 'type':'input'} + new = xmlUtils.newNode('alias', text=text.format(label + '_label'), attrib=attribs) + raven.append(new) + + # data handling: inner to outer data format + if case.data_handling['inner_to_outer'] == 'csv': + # swap the outputDatabase to outputExportOutStreams + output_node = template.find('Models').find('Code').find('outputDatabase') + output_node.tag = 'outputExportOutStreams' + # no need to change name, as database and outstream have the same name + + + +class GaussianProcessRegressor(FeatureDriver): + def __init__(self): + super().__init__() + self._name = "gpROM" + + def _modify_models(self, template, case, components, sources): + features = get_feature_list(case, components) + # TODO: Move default values to somewhere they could actually be reached, + # like somewhere in the case input specs. + model_params = { + "Features": ", ".join(features), + "Target": build_opt_metric_from_name(case), + "alpha": 1e-8, + "n_restarts_optimizer": 5, + "normalize_y": True, + "kernel": "Custom", + "custom_kernel": "(Constant*Matern)", + "anisotropic": True, + "multioutput": False + } + rom = EntityNode("ROM", "gpROM", "GaussianProcessRegressor", kwarg_subs=model_params) + + # Add to Models node + models = template.find("Models") + models.append(rom) + + +class EnsembleModel(FeatureDriver): + pass diff --git a/templates/feature_drivers/multiruns.py b/templates/feature_drivers/multiruns.py new file mode 100644 index 00000000..e69de29b diff --git a/templates/feature_drivers/naming_templates.py b/templates/feature_drivers/naming_templates.py new file mode 100644 index 00000000..31af9159 --- /dev/null +++ b/templates/feature_drivers/naming_templates.py @@ -0,0 +1,3 @@ +NAMING_TEMPLATES = { + "variable": "{unit}_{feature}" +} \ No newline at end of file diff --git a/templates/feature_drivers/optimizers.py b/templates/feature_drivers/optimizers.py new file mode 100644 index 00000000..33a3e453 --- /dev/null +++ b/templates/feature_drivers/optimizers.py @@ -0,0 +1,65 @@ +""" +Optimization features + +@author: Jacob Bryan (@j-bryan) +@date: 2024-11-08 +""" + +from feature_driver import FeatureDriver, FeatureCollection +from .samplers import StratifiedSampler +from .models import GaussianProcessRegressor + + +# TODO: move to template driver +# class Optimizer(FeatureCollection): +# def edit_template(self, template, case, components, sources): +# if case.get_opt_strategy() == "BayesianOpt": +# self._features = [BayesianOpt(), +# OptimizationSettings("BayesianOptimizer")] +# elif case.get_opt_strategy() == "GradientDescent": +# self._features = [GradientDescent(), +# OptimizationSettings("GradientDescent")] +# super().edit_template(template, case, components, sources) + + +class BayesianOptimizer(FeatureCollection): + def __init__(self): + super().__init__() + sampler_name = "LHS_samp" + gp_rom_name = "gpROM" + self._features = [ + BayesianOpt(sampler_name, gp_rom_name), + StratifiedSampler(sampler_name), + GaussianProcessRegressor(gp_rom_name), + OptimizationSettings("BayesianOptimizer")] + + +class BayesianOpt(FeatureDriver): + pass + + +class GradientDescentOptimizer(FeatureCollection): + def __init__(self): + super().__init__() + self._features = [GradientDescent(), + OptimizationSettings("GradientDescent")] + + +class GradientDescent(FeatureDriver): + pass + + +class OptimizationSettings(FeatureDriver): + """ + Defines common optimizer options + """ + def __init__(self, optimizer): + super().__init__() + self._optimizer = optimizer + + def _modify_optimizers(self, template, case, components, sources): + optimizer = template.find(f".//Optimizers/{self._optimizer}") + # convergence + # sampler init + # objective + # TargetEvaluation diff --git a/templates/feature_drivers/plots.py b/templates/feature_drivers/plots.py new file mode 100644 index 00000000..4386733f --- /dev/null +++ b/templates/feature_drivers/plots.py @@ -0,0 +1,71 @@ +# Copyright 2020, Battelle Energy Alliance, LLC +# ALL RIGHTS RESERVED +""" + Define plots for RAVEN workflows + + @author: Jacob Bryan (@j-bryan) + @date: 2024-11-08 +""" +from .feature_driver import FeatureDriver, FeatureCollection +from snippets import EntityNode, StepNode, AssemblerNode +from utils import add_step_to_sequence + + +class PlotBase(FeatureDriver): + def __init__(self, name, source, subType): + super().__init__() + self._name = name + self._subType = subType + self._plot_params = { + "source": source, + } + + def _modify_outstreams(self, template, case, components, sources): + # Create new node for the dispatch plot + plot = EntityNode("Plot", self._name, self._subType, self._plot_params) + + # Add plot to the OutStreams + outstreams = template.find("OutStreams") + outstreams.append(plot) + + def _modify_steps(self, template, case, components, sources): + # Make IOStep for plot + source = self.subs["source"] + source_dataobj = template.find(f"DataObjects/[@name='{source}']") + source_type = source_dataobj.tag + source_name = source_dataobj.get("name") + + plot_input = AssemblerNode("Input", "DataObjects", source_type, source_name) + plot_output = AssemblerNode("Output", "OutStreams", "Plot", self._name) + plot_io = StepNode("IOStep", self._name, [plot_input, plot_output]) + + steps = template.find("Steps") + steps.append(plot_io) + + def _modify_runinfo(self, template, case, components, sources): + # Add step to RunInfo sequnce + # TODO: how to ensure the IOStep comes in appropriate order (e.g. after MultiRun) + add_step_to_sequence(template, self._name) + +class HeronDispatchPlot(PlotBase): + def __init__(self, name="dispatch_plot", source="dispatch", subType="HERON.DispatchPlot"): + super().__init__(name, source, subType) + + def edit_template(self, template, case, components, sources): + self._plot_params["macro_variable"] = case.get_year_name() + self._plot_params["micro_variable"] = case.get_time_name() + + # Which signals to plot? + signals = set([src.get_variable() for src in sources]) + signals.discard(None) # pop None from the set, if it's in there + self._plot_params["signals"] = ", ".join(signals) + + super().edit_template(template, case, components, sources) + + +class TealCashFlowPlot(FeatureDriver): + """ + TEAL CashFlow plot + """ + def __init__(self, name="cashflow_plot", source="cashflows", subType="TEAL.CashFlowPlot"): + super().__init__(self, name, source, subType) diff --git a/templates/feature_drivers/samplers.py b/templates/feature_drivers/samplers.py new file mode 100644 index 00000000..2239262c --- /dev/null +++ b/templates/feature_drivers/samplers.py @@ -0,0 +1,55 @@ +""" +Sampler features + +@author: Jacob Bryan (@j-bryan) +@date: 2024-11-08 +""" +import xml.etree.ElementTree as ET + +from .feature_driver import FeatureDriver, FeatureCollection +from .naming_templates import NAMING_TEMPLATES +from .snippets import EntityNode + + +class Sampler(FeatureDriver): + def _modify_distributions(self, template, case, components, sources): + pass + + +class GridSampler(FeatureDriver): + def _modify_samplers(self, template, case, components, sources): + grid = ET.Element("Grid", attrib={"name": "grid"}) + # TODO: Move "denoises" node to something for SyntheticHistories? + denoises = ET.SubElement(grid, "constant", attrib={"name": "denoises"}) + denoises.text = case.get_num_samples() + + # add "GRO_case_labels" to sampler input if case has labels + # TODO: refactor to be handled for all applicable samplers and optimizers + for key, value in case.get_labels().items(): + label_name = NAMING_TEMPLATES['variable'].format(unit=key, feature='label') + node = ET.Element(grid, "constant", attrib={"name": label_name}) + node.text = value + + # TODO: this is modifying distributions + for key, value in case.dispatch_vars.items(): + var_name = self.namingTemplates['variable'].format(unit=key, feature='dispatch') + vals = value.get_value(debug=case.debug['enabled']) + if isinstance(vals, list): + dist, xml = self._create_new_sweep_capacity(key, var_name, vals, sampler) + dists_node.append(dist) + grid.append(xml) + +class MonteCarloSampler(FeatureDriver): + pass + +class StratifiedSampler(FeatureDriver): + pass + +class CustomSampler(FeatureDriver): + def __init__(self, name: str): + super().__init__() + self._name = "sampler" + + def _modify_samplers(self, template, case, components, sources): + sampler = EntityNode("CustomSampler", self._name) + # TODO diff --git a/templates/feature_drivers/snippets.py b/templates/feature_drivers/snippets.py new file mode 100644 index 00000000..9b59b9f5 --- /dev/null +++ b/templates/feature_drivers/snippets.py @@ -0,0 +1,48 @@ +""" +Utility functions to build common RAVEN XML snippets + +@author: Jacob Bryan (@j-bryan) +@date: 2024-11-08 +""" + +import xml.etree.ElementTree as ET + + +class EntityNode(ET.Element): + def __init__(self, tag: str, name: str, subType: str = "", kwarg_subs: dict[str, str] = {}): + """ + Create the Element. It's common for these Entities to have a number of subnodes which are + just tag/text pairs. We facilitate the creation of these subnodes by providing the "kwarg_subs" + argument. + """ + attrib = {"name": name} + if subType: + attrib["subType"] = subType + super().__init__(tag, attrib) + for tag, text in kwarg_subs.items(): + sub = ET.SubElement(self, tag) + sub.text = text + +class AssemblerNode(ET.Element): + """ + Assember nodes to go in Steps + """ + def __init__(self, tag, className, typeName, text): + attrib = {"class": className, + "type": typeName} + super().__init__(tag, attrib) + self.text = text + +class StepNode(ET.Element): + """ + Steps to go in block + """ + ASSEMBLER_NAMES = ["Function", "Input", "Model", "Sampler", "Optimizer", "SolutionExport", "Output"] + + def __init__(self, tag: str, name: str, subs: list[AssemblerNode]): + super().__init__(tag, {"name": name}) + # Add assembler nodes in the right order! + for assemb_name in self.ASSEMBLER_NAMES: + for sub in subs: + if sub.tag == assemb_name: + self.append(sub) diff --git a/templates/feature_drivers/utils.py b/templates/feature_drivers/utils.py new file mode 100644 index 00000000..189a8e4e --- /dev/null +++ b/templates/feature_drivers/utils.py @@ -0,0 +1,52 @@ +def add_to_comma_separated_list(s, val): + if s: + s += ", " + val + else: + return val + +def add_step_to_sequence(template, step): + sequence = template.find("RunInfo/Sequence") + steps = sequence.text + if steps: + steps += ", " + step + else: + steps = step + sequence.text = steps + +def build_opt_metric_from_name(case) -> str: + """ + Constructs the output name of the metric specified as the optimization objective. If no metric was + provided in the HERON XML, this defaults to "mean_NPV". + @ In, case, HERON Case, defining Case instance + @ Out, opt_out_metric_name, str, output metric name for use in inner/outer files + """ + try: + # metric name in RAVEN + optimization_settings = case.get_optimization_settings() + metric_raven_name = optimization_settings['stats_metric']['name'] + # potential metric name to add + opt_out_metric_name = case.stats_metrics_meta[metric_raven_name]['prefix'] + # do I need to add a percent or threshold to this name? + if metric_raven_name == 'percentile': + opt_out_metric_name += '_' + str(optimization_settings['stats_metric']['percent']) + elif metric_raven_name in ['valueAtRisk', 'expectedShortfall', 'sortinoRatio', 'gainLossRatio']: + opt_out_metric_name += '_' + str(optimization_settings['stats_metric']['threshold']) + opt_econ_metric, _ = case.get_opt_metric() + output_econ_metric_name = case.economic_metrics_meta[opt_econ_metric]['output_name'] + opt_out_metric_name += f'_{output_econ_metric_name}' + except (TypeError, KeyError): + # node not in input file OR + # 'metric' is missing from _optimization_settings + opt_out_metric_name = "mean_NPV" + + return opt_out_metric_name + +def get_feature_list(case, components) -> list[str]: + # TODO there must be a better way + feature_list = [] + for component in components: # get all interaction capacities which are features + interaction = component.get_interaction() + cap = interaction.get_capacity(None, raw=True) + if cap.is_parametric() and isinstance(cap.get_value(debug=case.debug['enabled']) , list): + feature_list.append(component.name + '_capacity') + return feature_list diff --git a/templates/feature_drivers/vargroups.py b/templates/feature_drivers/vargroups.py new file mode 100644 index 00000000..0c3a1bc4 --- /dev/null +++ b/templates/feature_drivers/vargroups.py @@ -0,0 +1,23 @@ +from .feature_driver import FeatureDriver, FeatureCollection + + +# class VariableGroup(FeatureDriver): +# pass + +class CapacitiesVarGroup(FeatureDriver): + pass + +class OuterResultsVarGroup(FeatureDriver): + pass + +class SyntheticHistoryVarGroup(FeatureDriver): + pass + +class CashFlowsVarGroup(FeatureDriver): + pass + +class DebugDispatchVarGroup(FeatureDriver): + pass + +class DebugVarGroups(FeatureCollection): + pass diff --git a/templates/flat.xml b/templates/flat.xml new file mode 100644 index 00000000..e69de29b diff --git a/templates/inner_static.xml b/templates/inner_static.xml new file mode 100644 index 00000000..f2b842d1 --- /dev/null +++ b/templates/inner_static.xml @@ -0,0 +1,121 @@ + + + + . + arma_sampling, summarize, database + 1 + + + + + dispatch_placeholder + sample_and_dispatch + mc_arma_dispatch + arma_metrics + + + arma_metrics + statistics + metrics_stats + + + metrics_stats + disp_results + + + + + GRO_dispatch_in, GRO_dispatch_out + GRO_dispatch_in_scalar, GRO_dispatch_in_Time + + GRO_capacities, scaling + + GRO_armasamples_in, GRO_armasamples_out + GRO_armasamples_in_scalar + GRO_armasamples_out_scalar + scaling, GRO_capacities + + + + + + stepwise + + + + + + + GRO_armasamples_in + GRO_armasamples_out + + + + + + GRO_final_return + + + GRO_dispatch_in + GRO_dispatch_in_Time + GRO_dispatch_in_Time + + + GRO_dispatch_in_scalar + + + + + + + + + + GRO_dispatch, GRO_armasamples + + + + dispatch + dispatch_placeholder + dispatch_eval + + + + + + + + + ../../heron.lib + + + + + + + + + 42 + 3 + + 1.0 + + + + + + + + + + + csv + arma_samples + + + csv + metrics_stats + + + + diff --git a/templates/inner_static_hists.xml b/templates/inner_static_hists.xml new file mode 100644 index 00000000..b70f5222 --- /dev/null +++ b/templates/inner_static_hists.xml @@ -0,0 +1,137 @@ + + + + . + arma_sampling, summarize, database + 1 + + + + dispatch_placeholder + sample_and_dispatch + mc_arma_dispatch + arma_metrics + + + disp_full + disp_full + + + arma_metrics + statistics + metrics_stats + + + + metrics_stats + disp_results + + + + + + GRO_dispatch_in, GRO_dispatch_out + GRO_dispatch_in_scalar, GRO_dispatch_in_Time + + GRO_capacities, scaling + + GRO_armasamples_in, GRO_armasamples_out + GRO_armasamples_in_scalar + GRO_armasamples_out_scalar + scaling, GRO_capacities + + + + + + stepwise + + + + + + + GRO_armasamples_in + GRO_armasamples_out + + + + + + GRO_final_return + + + GRO_full_dispatch, GRO_dispatch, GRO_cashflows + GRO_full_dispatch, GRO_dispatch_in_Time + GRO_full_dispatch, GRO_dispatch_in_Time + GRO_full_dispatch, GRO_dispatch_in_Time + GRO_cashflows + + + GRO_dispatch_in + GRO_dispatch_in_Time + GRO_dispatch_in_Time + + + GRO_dispatch_in_scalar + + + + + + + + + + + GRO_dispatch, GRO_armasamples + + + + dispatch + dispatch_placeholder + dispatch_eval + + + + + + + + + ../../heron.lib + + + + + + + + + 42 + 3 + + 1.0 + + + + + + + + + + + csv + arma_samples + + + csv + metrics_stats + + + + diff --git a/templates/inner_synth.xml b/templates/inner_synth.xml new file mode 100644 index 00000000..f2b842d1 --- /dev/null +++ b/templates/inner_synth.xml @@ -0,0 +1,121 @@ + + + + . + arma_sampling, summarize, database + 1 + + + + + dispatch_placeholder + sample_and_dispatch + mc_arma_dispatch + arma_metrics + + + arma_metrics + statistics + metrics_stats + + + metrics_stats + disp_results + + + + + GRO_dispatch_in, GRO_dispatch_out + GRO_dispatch_in_scalar, GRO_dispatch_in_Time + + GRO_capacities, scaling + + GRO_armasamples_in, GRO_armasamples_out + GRO_armasamples_in_scalar + GRO_armasamples_out_scalar + scaling, GRO_capacities + + + + + + stepwise + + + + + + + GRO_armasamples_in + GRO_armasamples_out + + + + + + GRO_final_return + + + GRO_dispatch_in + GRO_dispatch_in_Time + GRO_dispatch_in_Time + + + GRO_dispatch_in_scalar + + + + + + + + + + GRO_dispatch, GRO_armasamples + + + + dispatch + dispatch_placeholder + dispatch_eval + + + + + + + + + ../../heron.lib + + + + + + + + + 42 + 3 + + 1.0 + + + + + + + + + + + csv + arma_samples + + + csv + metrics_stats + + + + diff --git a/templates/inner_synth_hists.xml b/templates/inner_synth_hists.xml new file mode 100644 index 00000000..f63cf5d1 --- /dev/null +++ b/templates/inner_synth_hists.xml @@ -0,0 +1,121 @@ + + + + . + arma_sampling, summarize, database + 1 + + + + + dispatch_placeholder + sample_and_dispatch + mc_arma_dispatch + arma_metrics + + + arma_metrics + statistics + metrics_stats + + + metrics_stats + disp_results + + + + + GRO_dispatch_in, GRO_dispatch_out + GRO_dispatch_in_scalar, GRO_dispatch_in_Time + + GRO_capacities, scaling + + GRO_armasamples_in, GRO_armasamples_out + GRO_armasamples_in_scalar + GRO_armasamples_out_scalar + scaling, GRO_capacities + + + + + + stepwise + + + + + + + GRO_armasamples_in + GRO_armasamples_out + + + + + + GRO_final_return + + + GRO_dispatch_in + GRO_dispatch_in_Time + GRO_dispatch_in_Time + + + GRO_dispatch_in_scalar + + + + + + + + + + GRO_dispatch, GRO_armasamples + + + + dispatch + dispatch_placeholder + dispatch_eval + + + + + + + + + ../../heron.lib + + + + + + + + + 42 + 3 + + 1.0 + + + + + + + + + + + csv + arma_samples + + + csv + metrics_stats + + + + diff --git a/templates/outer_debug.xml b/templates/outer_debug.xml new file mode 100644 index 00000000..a439b85b --- /dev/null +++ b/templates/outer_debug.xml @@ -0,0 +1,105 @@ + + + + . + debug, debug_output + 1 + + + + + inner_workflow + heron_lib + raven + grid + grid + sweep + + + dispatch + cashflows + dispatch_print + cashflows + + + + + + + + + + + + + + + + scaling + GRO_outer_debug_dispatch,GRO_outer_debug_synthetics + GRO_outer_debug_dispatch, GRO_outer_debug_synthetics + GRO_outer_debug_dispatch, GRO_outer_debug_synthetics + GRO_outer_debug_dispatch, GRO_outer_debug_synthetics + + + GRO_outer_debug_cashflows + + cfYears + + + + + + + ~/projects/raven/raven_framework + disp_results + + + + Samplers|MonteCarlo@name:mc_arma_dispatch|constant@name:denoises + + + + + + + + + 1 + + 1 + + + + + + inner.xml + ../heron.lib + + + + + GRO_outer_debug_dispatch, GRO_outer_debug_synthetics + + + + + + csv + dispatch + + + csv + cashflows + + + + diff --git a/templates/outer_opt.xml b/templates/outer_opt.xml new file mode 100644 index 00000000..4849d721 --- /dev/null +++ b/templates/outer_opt.xml @@ -0,0 +1,166 @@ + + + + . + optimize, plot + 1 + + + + + inner_workflow + heron_lib + raven + cap_opt + opt_eval + opt_soln + opt_soln + + + opt_soln + opt_path + opt_soln + + + + + + + + + + + + + + GRO_capacities + GRO_outer_results + + + trajID + iteration, accepted, GRO_capacities, GRO_outer_results + + + + + + ~/projects/raven/raven_framework + disp_results + + + + Samplers|MonteCarlo@name:mc_arma_dispatch|constant@name:denoises + + + + mean_NPV + 1e-8 + 5 + True + Custom + (Constant*Matern) + True + False + + + + + + + + + + + + + + + mean_NPV + 1 + opt_eval + + 800 + every + max + + + + + + + 2 + 1.5 + 0.2 + + + + + + + 1 + 1e-4 + 1e-8 + + + + mean_NPV + 1 + opt_eval + + 100 + max + every + + LHS_samp + gpROM + + 1 + Internal + + + 1e-5 + 4 + + + + differentialEvolution + 30 + + + differentialEvolution + 30 + 1 + 20 + Constant + + + differentialEvolution + 30 + 0.98 + Constant + + + + + + + inner.xml + ../heron.lib + + + + + csv + grid + + + csv + opt_soln + trajID + + + opt_soln + GRO_capacities + + + diff --git a/templates/outer_sweep.xml b/templates/outer_sweep.xml new file mode 100644 index 00000000..77d1ba37 --- /dev/null +++ b/templates/outer_sweep.xml @@ -0,0 +1,63 @@ + + + + . + sweep + 1 + + + + + inner_workflow + heron_lib + raven + grid + grid + sweep + + + + + + + + + + + GRO_capacities + GRO_outer_results + + + + + + ~/projects/raven/raven_framework + disp_results + + + + Samplers|MonteCarlo@name:mc_arma_dispatch|constant@name:denoises + + + + + + + + + 1 + + + + + inner.xml + ../heron.lib + + + + + csv + grid + + + From f5c8d541edf9599c612bf18f6bc6ce67e15969c7 Mon Sep 17 00:00:00 2001 From: Jacob Bryan Date: Mon, 18 Nov 2024 09:10:15 -0700 Subject: [PATCH 03/54] some feature drivers --- src/Cases.py | 17 +- templates/feature_drivers/feature_driver.py | 11 +- templates/feature_drivers/models.py | 113 +- templates/feature_drivers/print.py | 15 + templates/feature_drivers/samplers.py | 5 +- templates/feature_drivers/snippets.py | 2 +- templates/feature_drivers/utils.py | 4 + templates/raven_templates.py | 131 + templates/template_driver.py | 3876 ++++++++++--------- 9 files changed, 2226 insertions(+), 1948 deletions(-) create mode 100644 templates/feature_drivers/print.py create mode 100644 templates/raven_templates.py diff --git a/src/Cases.py b/src/Cases.py index adac039c..931b736c 100644 --- a/src/Cases.py +++ b/src/Cases.py @@ -1361,26 +1361,25 @@ def write_workflows(self, components, sources, loc): @ In, loc, str, location in which to write files @ Out, None """ - # load templates - driver = self._load_template(components, sources) - driver.create_workflows(self, components, sources) - driver.write_workflows(loc) + # Load templates, create RAVEN workflows, and write those workflows using a TemplateDriver + driver = self._make_template_driver() + workflows = driver.create_workflows(self, components, sources) # list[RavenTemplate] + for workflow in workflows: + workflow.writeWorkflow(loc) #### UTILITIES #### - def _load_template(self, components, sources): + def _make_template_driver(self): """ Loads template files for modification - @ In, components, HERON components, components for the simulation - @ In, sources, HERON sources, sources for the simulation + @ In, None @ Out, template_class, TemplateDriver, instantiated TemplateDriver class """ src_dir = os.path.dirname(os.path.realpath(__file__)) heron_dir = os.path.abspath(os.path.join(src_dir, '..')) - template_dir = os.path.abspath(os.path.join(heron_dir, 'templates')) template_name = 'template_drivers' # import template module sys.path.append(heron_dir) module = importlib.import_module(f'templates.{template_name}', package="HERON") # load template, perform actions - driver = module.create_template_driver(self, components, sources) + driver = module.TemplateDriver() return driver diff --git a/templates/feature_drivers/feature_driver.py b/templates/feature_drivers/feature_driver.py index 7b311af5..9402beee 100644 --- a/templates/feature_drivers/feature_driver.py +++ b/templates/feature_drivers/feature_driver.py @@ -20,10 +20,13 @@ class FeatureDriver: FeatureDrivers translate HERON features to RAVEN workflow template changes. This makes feature logic more portable across different templates. """ - @property - @abstractmethod - def allowed_templates(self): - pass + def __init__(self, name: str): + """ + Feature driver constructor + + @ In, name, str, name of the feature + """ + self.name = name def edit_template(self, template, case, components, sources): """ diff --git a/templates/feature_drivers/models.py b/templates/feature_drivers/models.py index 5c7d4bbe..f96280b5 100644 --- a/templates/feature_drivers/models.py +++ b/templates/feature_drivers/models.py @@ -5,7 +5,7 @@ from .feature_driver import FeatureDriver from .snippets import EntityNode -from .utils import build_opt_metric_from_name, get_feature_list +from .utils import build_opt_metric_from_name, get_feature_list, get_subelement # load utils sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..')) @@ -16,68 +16,91 @@ RAVEN_LOC = os.path.abspath(os.path.join(hutils.get_raven_loc(), "ravenframework")) -class RavenCodeModel(FeatureDriver): +class RavenCode(FeatureDriver): """ - Sets up a model to run inner RAVEN workflow + Sets up a model to run inner RAVEN workflow. """ def _modify_models(self, template, case, components, sources): - raven = template.find(".//Models/Code[@subType='RAVEN']") - raven_exec = raven.find('executable') - raven_exec_guess = os.path.abspath(os.path.join(RAVEN_LOC, '..', 'raven_framework')) + # We require the template to already define a node for the inner RAVEN run, which we modify here. + # TODO: + raven = template.find("Models/Code[@subType='RAVEN']") + if not raven: + models = template.find("Models") + raise ValueError(f"No node found in the template XML! Children of node found: {[child.tag for child in models]}") + + # Where is the RAVEN executable? + executable = raven.find("executable") # NOTE: returns None if node not found + if not executable: # make node if not found + executable = ET.SubElement(raven, "executable") + raven_exec_guess = os.path.abspath(os.path.join(RAVEN_LOC, "..", "raven_framework")) if os.path.exists(raven_exec_guess): - raven_exec.text = raven_exec_guess + executable.text = raven_exec_guess elif shutil.which("raven_framework") is not None: - raven_exec.text = "raven_framework" + executable.text = "raven_framework" else: raise RuntimeError("raven_framework not in PATH and not at "+raven_exec_guess) + # custom python command for running raven (for example, "coverage run") - if case.get_py_cmd_for_raven() is not None: - attribs = {'type': 'prepend', 'arg': case.get_py_cmd_for_raven()} - new = ET.Element('clargs', attrib=attribs) + if cmd := case.get_py_cmd_for_raven(): + attribs = {"type": "prepend", "arg": cmd} + new = ET.Element("clargs", attrib=attribs) raven.append(new) - # conversion script - conv = raven.find('conversion').find('input') - conv.attrib['source'] = '../write_inner.py' - # Set variable aliases for Inner - alias_template = 'Samplers|MonteCarlo@name:mc_arma_dispatch|constant@name:{}' + # conversion script + conv = raven.find("conversion") + if conv is None: + conv = ET.SubElement(raven, "conversion") + conv_inp = conv.find("input") + if conv_inp is None: + conv_inp = ET.SubElement(conv, "input") + conv_inp.attrib["source"] = "../write_inner.py" + + # Add variable aliases for Inner + alias_template = "Samplers|MonteCarlo@name:mc_arma_dispatch|constant@name:{}" for component in components: - name = component.name - attribs = {"variable": f"{name}_capacity", "type": "input"} - alias = ET.Element("alias", attribs) - alias.text = alias_template.format() - raven.append(alias) - - # # label aliases placed inside models - # for label in case.get_labels(): - # attribs = {'variable': f'{label}_label', 'type':'input'} - # new = xmlUtils.newNode('alias', text=text.format(label + '_label'), attrib=attribs) - # raven.append(new) - - # # data handling: inner to outer data format - # if case.data_handling['inner_to_outer'] == 'csv': - # # swap the outputDatabase to outputExportOutStreams - # output_node = template.find('Models').find('Code').find('outputDatabase') - # output_node.tag = 'outputExportOutStreams' - # # no need to change name, as database and outstream have the same name + attribs = {"variable": f"{component.name}_capacity", "type": "input"} + alias = ET.SubElement(raven, "alias", attribs) + alias.text = alias_template.format(component.name + "_capacity") - -class InnerDataHandling(FeatureDriver): - def _modify_models(self, template, case, components, sources): - # label aliases placed inside models + # Add label aliases placed inside models for label in case.get_labels(): attribs = {'variable': f'{label}_label', 'type':'input'} - new = xmlUtils.newNode('alias', text=text.format(label + '_label'), attrib=attribs) - raven.append(new) + label_alias = ET.SubElement(raven, "alias", attrib=attribs) + label_alias.text = alias_template.format(label + "_label") # data handling: inner to outer data format - if case.data_handling['inner_to_outer'] == 'csv': - # swap the outputDatabase to outputExportOutStreams - output_node = template.find('Models').find('Code').find('outputDatabase') - output_node.tag = 'outputExportOutStreams' - # no need to change name, as database and outstream have the same name + output_tags = {"netcdf": "outputDatabase", + "csv": "outputExportOutStreams"} + output_node = ET.SubElement(raven, output_tags.get(case.data_handling["inner_to_outer"])) + output_node.text = "disp_results" + + def _modify_databases(self, template, case, components, sources): + if case.data_handling["inner_to_outer"] == "netcdf": + databases = template.find("Databases") + disp_results = ET.SubElement(databases, "") + +class InnerDataHandling(FeatureDriver): + def __init__(self, model_name: str): + super().__init__("") # no name for this feature itself + self._model_name = model_name + + def edit_template(self, template, case, components, sources): + # inner to outer data format + if case.data_handling["inner_to_outer"] == "netcdf": + self._database_handling(template, case, components, sources) + elif case.data_handling["inner_to_outer"] == "csv": + self._csv_handling(template, case, components, sources) + else: + raise ValueError(f"Unrecognized inner to outer data handling type '{case.data_handling['inner_to_outer']}'") + # def _modify_models(self, template, case, components, sources): + # # inner to outer data format + # output_tag = "outputExportOutStreams" if case.data_handling["inner_to_outer"] == "csv" else "outputDatabase" + # output_node = ET.SubElement(raven, output_tag) + # output_node.text = "disp_results" + def _database_handling(self, template, case, components, sources): + output_tag = "outputExportOutStreams" class GaussianProcessRegressor(FeatureDriver): def __init__(self): diff --git a/templates/feature_drivers/print.py b/templates/feature_drivers/print.py new file mode 100644 index 00000000..f1e2de94 --- /dev/null +++ b/templates/feature_drivers/print.py @@ -0,0 +1,15 @@ +from .feature_driver import FeatureDriver, FeatureCollection + + +class Print(FeatureDriver): + def _modify_steps(self, template, case, components, sources): + # Add IOStep to steps with DataObjects and OutStreams + pass + + def _modify_outstreams(self, template, case, components, sources): + # Add Print outstream node + pass + + def _modify_runinfo(self, template, case, components, sources): + # Add IOStep to sequence + pass diff --git a/templates/feature_drivers/samplers.py b/templates/feature_drivers/samplers.py index 2239262c..99b15794 100644 --- a/templates/feature_drivers/samplers.py +++ b/templates/feature_drivers/samplers.py @@ -31,8 +31,11 @@ def _modify_samplers(self, template, case, components, sources): node.text = value # TODO: this is modifying distributions + + def _modify_distributions(self, template, case, components, sources): + dists_node = template.find("") for key, value in case.dispatch_vars.items(): - var_name = self.namingTemplates['variable'].format(unit=key, feature='dispatch') + var_name = template.namingTemplates['variable'].format(unit=key, feature='dispatch') vals = value.get_value(debug=case.debug['enabled']) if isinstance(vals, list): dist, xml = self._create_new_sweep_capacity(key, var_name, vals, sampler) diff --git a/templates/feature_drivers/snippets.py b/templates/feature_drivers/snippets.py index 9b59b9f5..76f18d77 100644 --- a/templates/feature_drivers/snippets.py +++ b/templates/feature_drivers/snippets.py @@ -25,7 +25,7 @@ def __init__(self, tag: str, name: str, subType: str = "", kwarg_subs: dict[str, class AssemblerNode(ET.Element): """ - Assember nodes to go in Steps + Assembler nodes to go in Steps """ def __init__(self, tag, className, typeName, text): attrib = {"class": className, diff --git a/templates/feature_drivers/utils.py b/templates/feature_drivers/utils.py index 189a8e4e..13a72896 100644 --- a/templates/feature_drivers/utils.py +++ b/templates/feature_drivers/utils.py @@ -1,3 +1,7 @@ +from typing import Any +import xml.etree.ElementTree as ET + + def add_to_comma_separated_list(s, val): if s: s += ", " + val diff --git a/templates/raven_templates.py b/templates/raven_templates.py new file mode 100644 index 00000000..481ea062 --- /dev/null +++ b/templates/raven_templates.py @@ -0,0 +1,131 @@ +# Copyright 2020, Battelle Energy Alliance, LLC +# ALL RIGHTS RESERVED +""" + RAVEN workflow templates + + @author: j-bryan + @date: 2024-10-29 +""" +import sys +import os +import xml.etree.ElementTree as ET + +from .feature_drivers import FeatureDriver + +# load utils +sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..')) +from HERON.src.base import Base +from HERON.src.Cases import Case +from HERON.src.Components import Component +from HERON.src.Placeholders import ARMA, CSV +import HERON.src._utils as hutils +sys.path.pop() + +RAVEN_LOC = os.path.abspath(os.path.join(hutils.get_raven_loc(), "ravenframework")) + +sys.path.append(os.path.join(RAVEN_LOC, '..')) +from ravenframework.InputTemplates.TemplateBaseClass import Template +sys.path.pop() + + +class RavenTemplate(Template): + """ Template class for RAVEN workflows """ + Template.addNamingTemplates({'jobname' : '{case}_{io}', + 'stepname' : '{action}_{subject}', + 'variable' : '{unit}_{feature}', + 'dispatch' : 'Dispatch__{component}__{tracker}__{resource}', + 'tot_activity' :'{stats}_TotalActivity__{component}__{tracker}__{resource}', + 'data object' : '{source}_{contents}', + 'distribution' : '{unit}_{feature}_dist', + 'ARMA sampler' : '{rom}_sampler', + 'lib file' : 'heron.lib', # TODO use case name? + 'cashfname' : '_{component}{cashname}', + 're_cash' : '_rec_{period}_{driverType}{driverName}', + 'cluster_index' : '_ROM_Cluster', + 'metric_name' : '{stats}_{econ}', + }) + + def __init__(self): + super().__init__() + self.features = [] + + ###################### + # RAVEN Template API # + ###################### + def loadTemplate(self): + # TODO + # load XML from file + pass + + def createWorkflow(self, case: Case, components: list[Component], sources: list[CSV | ARMA]): + """ + Create workflow by applying feature changes to the template XML + + @ In, case, HERON.src.Cases.Case + @ In, components, list[HERON.src.Components.Component] + @ In, sources, TODO + @ Out, None + """ + for feature in self.features: + feature.edit_template(self._template, case, components, sources) + return self + + def writeWorkflow(self): + # TODO + pass + + ############################ + # XML manipulation methods # + ############################ + def find(self, match: str, namespaces: str = None) -> ET.Element | None: + """ + Find the first node with a matching tag or path in the template XML. Wraps the + xml.etree.ElementTree.Element.find() method. + + @ In, match, str, string to match tag name or path + @ In, namespaces, str, optional, an optional mapping form namespace prefix to full name + @ Out, node, ET.Element | None, first element with matching tag or None if no matches are found + """ + return self._template.find(match, namespaces) + + def findall(self, match: str, namespaces: str = None) -> list[ET.Element] | None: + """ + Find all nodes with a matching tag or path in the template XML. Wraps the + xml.etree.ElementTree.Element.findall() method. + + @ In, match, str, string to match tag name or path + @ In, namespaces, str, optional, an optional mapping form namespace prefix to full name + @ Out, nodes, list[ET.Element] | None, first element with matching tag or None if no matches are found + """ + return self._template.findall(match, namespaces) + + ################################ + # Feature modification methods # + ################################ + def add_features(self, *feats: FeatureDriver): + """ + Add features to the template XML + + @ In, feats, FeatureDriver, one or more features to add to the template XML + @ Out, None + """ + self.features.extend(feats) + +# Templates for specific workflow types +class FlatOneHistoryTemplate(Template): + Template.addNamingTemplates({"model_name": "dispatch"}) + + +class FlatFixedCapacitiesTemplate(Template): + Template.addNamingTemplates({"model_name": "dispatch"}) + + +class BilevelOuterTemplate(Template): + Template.addNamingTemplates({"model_name": "raven", + "sampler_name": "sampler", + "optimize_namer": "opt"}) + + +class BilevelInnerTemplate(Template): + Template.addNamingTemplates({"model_name": "dispatch", + "sampler_name": "sampler"}) diff --git a/templates/template_driver.py b/templates/template_driver.py index 54046104..48d4ccd6 100644 --- a/templates/template_driver.py +++ b/templates/template_driver.py @@ -14,6 +14,15 @@ import numpy as np import dill as pk +from .raven_templates import (FlatOneHistoryTemplate, + FlatFixedCapacitiesTemplate, + BilevelOuterTemplate, + BilevelInnerTemplate) +from .feature_drivers.samplers import GridSampler, CustomSampler, MonteCarloSampler +from .feature_drivers.optimizers import BayesianOptimizer, GradientDescent +from .feature_drivers.debug import DebugPlots +from .feature_drivers.histories import StaticHistory, SyntheticHistory + # load utils sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..')) from HERON.src.base import Base @@ -22,1911 +31,2002 @@ # get raven location RAVEN_LOC = os.path.abspath(os.path.join(hutils.get_raven_loc(), "ravenframework")) -try: - import TEAL.src -except ModuleNotFoundError: - CF_LOC = hutils.get_cashflow_loc(raven_path=RAVEN_LOC) - if CF_LOC is None: - raise RuntimeError('TEAL has not been found!\n' + - f'Check TEAL installation for the RAVEN at "{RAVEN_LOC}"') - - sys.path.append(os.path.join(CF_LOC, '..')) -from TEAL.src.main import getProjectLength -from TEAL.src import CashFlows sys.path.append(os.path.join(RAVEN_LOC, '..')) from ravenframework.utils import xmlUtils from ravenframework.InputTemplates.TemplateBaseClass import Template as TemplateBase sys.path.pop() -# default stats abbreviations -DEFAULT_STATS_NAMES = ['expectedValue', 'sigma', 'median'] -SWEEP_DEFAULT_STATS_NAMES = ['maximum', 'minimum', 'percentile', 'samples', 'variance'] - -# prefixes for financial metrics only -FINANCIAL_PREFIXES = ["sharpe", "sortino", "es", "VaR", "glr"] - -class Template(TemplateBase, Base): - """ - Template for lcoe sweep opt class - This templates the workflow split into sweeping over unit capacities - in an OUTER run while optimizing unit dispatch in a INNER run. - - As designed, the ARMA stochastic noise happens entirely on the INNER, - for easier parallelization. - """ - - # dynamic naming templates - TemplateBase.addNamingTemplates({'jobname' : '{case}_{io}', - 'stepname' : '{action}_{subject}', - 'variable' : '{unit}_{feature}', - 'dispatch' : 'Dispatch__{component}__{tracker}__{resource}', - 'tot_activity' :'{stats}_TotalActivity__{component}__{tracker}__{resource}', - 'data object' : '{source}_{contents}', - 'distribution' : '{unit}_{feature}_dist', - 'ARMA sampler' : '{rom}_sampler', - 'lib file' : 'heron.lib', # TODO use case name? - 'cashfname' : '_{component}{cashname}', - 're_cash' : '_rec_{period}_{driverType}{driverName}', - 'cluster_index' : '_ROM_Cluster', - 'metric_name' : '{stats}_{econ}', - }) - - # template nodes - dist_template = xmlUtils.newNode('Uniform') - dist_template.append(xmlUtils.newNode('lowerBound')) - dist_template.append(xmlUtils.newNode('upperBound')) - - var_template = xmlUtils.newNode('variable') - var_template.append(xmlUtils.newNode('distribution')) - - ############ - # API # - ############ - def __repr__(self): - """ - String representation of this Handler and its VP - @ In, None - @ Out, repr, str, string representation - """ - msg = f'