Skip to content

Commit 8cf54cb

Browse files
authored
Merge pull request #415 from j-bryan/misc_fixes
Print statements, npv_exempt cash flows, dispatch plot fixes
2 parents 379c746 + 2c53fd6 commit 8cf54cb

File tree

11 files changed

+125
-53
lines changed

11 files changed

+125
-53
lines changed

src/Cases.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -281,10 +281,12 @@ def _specs_debug(cls):
281281
debug.addSub(InputData.parameterInputFactory('macro_steps', contentType=InputTypes.IntegerType,
282282
descr=r"""sets the number of macro steps (e.g. years) the stochastic synthetic histories and dispatch
283283
optimization should include. \default{1}"""))
284-
debug.addSub(InputData.parameterInputFactory('dispatch_plot', contentType=InputTypes.BoolType,
284+
dispatch_plot = InputData.parameterInputFactory('dispatch_plot', contentType=InputTypes.BoolType,
285285
descr=r"""provides a dispatch plot after running through \xmlNode{inner_samples} and
286286
\xmlNode{macro_steps} provided. To prevent plotting output during debug mode set to "False".
287-
\default{True}"""))
287+
\default{True}""")
288+
dispatch_plot.addParam('sep_by_resource', param_type=InputTypes.BoolType, descr=r"""produce one figure per resource. \default{False}""")
289+
debug.addSub(dispatch_plot)
288290
debug.addSub(InputData.parameterInputFactory('cashflow_plot', contentType=InputTypes.BoolType,
289291
descr=r"""provides a cashflow plot after running through \xmlNode{inner_samples} and
290292
\xmlNode{macro_steps} provided. To prevent plotting output during debug mode set to "False".
@@ -656,6 +658,7 @@ def __init__(self, run_dir, **kwargs):
656658
'inner_samples': 1, # how many inner realizations to sample
657659
'macro_steps': 1, # how many "years" for inner realizations
658660
'dispatch_plot': True, # whether to output a dispatch plot in debug mode
661+
'disp_plot_sep': False, # whether to separate dispatch plots into one figure per resource
659662
'cashflow_plot': True # whether to output a cashflow plot in debug mode
660663
}
661664

@@ -694,6 +697,8 @@ def read_input(self, xml):
694697
self.debug['enabled'] = True
695698
for node in item.subparts:
696699
self.debug[node.getName()] = node.value
700+
if node.getName() == 'dispatch_plot':
701+
self.debug['disp_plot_sep'] = node.parameterValues.get('sep_by_resource', False)
697702
elif item.getName() == 'label':
698703
self._labels[item.parameterValues['name']] = item.value
699704
if item.getName() == 'verbosity':

src/DispatchManager.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -571,10 +571,8 @@ def _segment_cashflow(self, meta, s, seg, year, dispatch, multiplicity,
571571
specific_meta['HERON']['all_activity'] = dispatch
572572
specific_activity = {}
573573
final_cashflows = final_comp.getCashflows()
574-
for f, heron_cf in enumerate(comp.get_cashflows()):
574+
for f, heron_cf in enumerate(filter(lambda cf: not cf.is_npv_exempt(), comp.get_cashflows())):
575575
# get the corresponding TEAL.CashFlow
576-
if heron_cf.is_npv_exempt():
577-
continue # Skip adding this cashflow to the final cashflows
578576
teal_cf = teal_comp.getCashflows()[f]
579577
final_cf = final_cashflows[f]
580578
# sanity continued

src/DispatchPlot.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ def getInputSpecification(cls):
4949
specs.addSub(InputData.parameterInputFactory('macro_variable', contentType=InputTypes.StringType))
5050
specs.addSub(InputData.parameterInputFactory('micro_variable', contentType=InputTypes.StringType))
5151
specs.addSub(InputData.parameterInputFactory('signals', contentType=InputTypes.StringListType))
52+
specs.addSub(InputData.parameterInputFactory('sep_by_resource', contentType=InputTypes.BoolType))
5253
return specs
5354

5455
def __init__(self):
@@ -63,6 +64,7 @@ def __init__(self):
6364
self._source = None
6465
self._macroName = None
6566
self._microName = None
67+
self._sepByResource = False
6668
self._addSignals = []
6769

6870
def handleInput(self, spec):
@@ -81,6 +83,8 @@ def handleInput(self, spec):
8183
self._microName = node.value
8284
elif node.getName() == 'signals':
8385
self._addSignals = node.value
86+
elif node.getName() == 'sep_by_resource':
87+
self._sepByResource = node.value
8488

8589
def initialize(self, stepEntities):
8690
"""
@@ -329,10 +333,19 @@ def run(self):
329333
# nature of the subplots, as well as the dynamic number of
330334
# components and signals to plot (i.e. dynamically nested subplots)
331335

332-
# If only 3 resources, make one figure; otherwise, 2 resources per figure
333-
if len(resources) <= 3:
336+
# Use a separate figure for each resource, if requested.
337+
if self._sepByResource:
338+
res_figs = []
339+
res_axs = []
340+
for _ in range(len(resources)):
341+
fig, axs = plt.subplots()
342+
res_figs.append(fig)
343+
res_axs.append(axs)
344+
# Otherwise, if only 3 resources, make one figure
345+
elif len(resources) <= 3:
334346
fig, res_axs = plt.subplots(len(resources), 1, sharex=True, squeeze=False)
335347
res_figs = [fig]
348+
# Otherwise if more than 3 resources, plot 2 resources per figure
336349
else:
337350
res_figs = []
338351
res_axs = []

templates/bilevel_templates.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -99,25 +99,22 @@ def createWorkflow(self, **kwargs) -> dict[str, ET.Element]:
9999

100100
return {"inner": self.inner.template_xml, "outer": self.outer.template_xml}
101101

102-
def writeWorkflow(self, template: dict[str, ET.Element], destination: dict[str, str], run: bool = False) -> None:
102+
def writeWorkflow(self, dest_dir: str) -> None:
103103
"""
104104
Writes a template to file.
105-
@ In, template, dict[str, ET.Element], XML trees to write
106-
@ In, destination, dict[str, str], paths to write the templates to
107-
@ In, run, bool, optional, if True then run the workflow after writing? good idea?
108-
@ Out, errors, int, 0 if successfully wrote [and run] and nonzero if there was a problem
105+
@ In, dest_dir, str, path to the directory to which to write template workflows
106+
@ Out, None
109107
"""
110-
for name, xml in template.items():
111-
super().writeWorkflow(xml, destination[name], run)
108+
self.inner.writeWorkflow(dest_dir)
109+
self.outer.writeWorkflow(dest_dir)
112110

113111
# copy "write_inner.py", which has the denoising and capacity fixing algorithms
114112
conv_filename = "write_inner.py"
115113
write_inner_dir = Path(__file__).parent
116-
dest_dir = Path(next(iter(destination.values()))).parent
117114
conv_src = write_inner_dir / conv_filename
118-
conv_file = dest_dir / conv_filename
115+
conv_file = Path(dest_dir) / conv_filename
119116
shutil.copyfile(str(conv_src), str(conv_file))
120-
print(f"Wrote '{conv_filename}' to '{destination}'")
117+
print(f"Wrote '{conv_filename}' to '{str(conv_file)}'")
121118

122119
@property
123120
def template_xml(self) -> dict[str, ET.Element]:

templates/debug_template.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ def _make_dispatch_plot(self, case: HeronCase) -> HeronDispatchPlot:
268268
disp_plot.macro_variable = case.get_year_name()
269269
disp_plot.micro_variable = case.get_time_name()
270270
disp_plot.signals.append("GRO_debug_synthetics")
271+
disp_plot.sep_by_resource = case.debug["disp_plot_sep"]
271272
return disp_plot
272273

273274
def _make_cashflow_plot(self) -> TealCashFlowPlot:

templates/raven_template.py

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import itertools as it
1414
import xml.etree.ElementTree as ET
1515

16-
from .imports import xmlUtils, Template
16+
from .imports import Template
1717
from .heron_types import HeronCase, Component, Source, ValuedParam
1818
from .naming_utils import get_result_stats, get_component_activity_vars, get_opt_objective, get_statistics, Statistic
1919
from .xml_utils import add_node_to_tree, stringify_node_values
@@ -26,7 +26,7 @@
2626
from .snippets.models import GaussianProcessRegressor, PickledROM, EnsembleModel
2727
from .snippets.distributions import Distribution, Uniform
2828
from .snippets.outstreams import PrintOutStream
29-
from .snippets.dataobjects import DataObject, PointSet, DataSet
29+
from .snippets.dataobjects import PointSet, DataSet
3030
from .snippets.variablegroups import VariableGroup
3131
from .snippets.files import File
3232
from .snippets.factory import factory as snippet_factory
@@ -118,27 +118,26 @@ def createWorkflow(self, **kwargs) -> None:
118118
# Universal workflow settings
119119
self._set_verbosity(kwargs["case"].get_verbosity())
120120

121-
def writeWorkflow(self, template: ET.Element, destination: str, run: bool = False) -> None:
121+
def writeWorkflow(self, dest_dir: str) -> None:
122122
"""
123123
Writes a template to file.
124-
@ In, template, xml.etree.ElementTree.Element, file to write
125-
@ In, destination, str, path and filename to write to
126-
@ In, run, bool, optional, if True then run the workflow after writing? good idea?
127-
@ Out, errors, int, 0 if successfully wrote [and run] and nonzero if there was a problem
124+
@ In, dest_dir, str, path to the directory to which to write template workflows
125+
@ Out, None
128126
"""
129127
# Ensure all node attribute values and text are expressed as strings. Errors are thrown if any of these aren't
130128
# strings. Enforcing this here allows flexibility with how node values are stored and manipulated before write
131129
# time, such as storing values as lists or numeric types. For example, text fields which are a comma-separated
132130
# list of values can be stored in the RavenSnippet object as a list, and new items can be inserted into that
133131
# list as needed, then the list can be converted to a string only now at write time.
134-
stringify_node_values(template)
132+
stringify_node_values(self._template)
135133

136134
# Remove any unused top-level nodes (Models, Samplers, etc.) to keep things looking clean
137-
for node in template:
135+
for node in self._template:
138136
if len(node) == 0:
139-
template.remove(node)
137+
self._template.remove(node)
140138

141-
super().writeWorkflow(template, destination, run)
139+
destination = self.get_write_path(dest_dir)
140+
super().writeWorkflow(self._template, destination)
142141
print(f"Wrote '{self.write_name}' to '{destination}'")
143142

144143
@property
@@ -221,7 +220,7 @@ def _set_case_name(self, name: str) -> None:
221220
@ In, name, str, case name to use
222221
@ Out, None
223222
"""
224-
run_info = self._template.find("RunInfo") # type: RunInfo
223+
run_info: RunInfo = self._template.find("RunInfo")
225224
run_info.job_name = name
226225
run_info.working_dir = name
227226

@@ -232,7 +231,7 @@ def _add_step_to_sequence(self, step: Step, index: int | None = None) -> None:
232231
@ In, index, int, optional, the index to add the step at
233232
@ Out, None
234233
"""
235-
run_info = self._template.find("RunInfo") # type: RunInfo
234+
run_info: RunInfo = self._template.find("RunInfo")
236235
idx = index if index is not None else len(run_info.sequence)
237236
run_info.sequence.insert(idx, step)
238237

@@ -264,7 +263,7 @@ def _load_file_to_object(self, source: Source, target: RavenSnippet) -> IOStep:
264263
@ Out, step, IOStep, the step used to do the loading
265264
"""
266265
# Get the file to load. Might already exist in the template XML
267-
file = self._template.find("Files/Input[@name='{source.name}']") # type: File
266+
file: File | None = self._template.find("Files/Input[@name='{source.name}']")
268267
if file is None:
269268
file = File(source.name)
270269
file.path = source._target_file
@@ -416,14 +415,14 @@ def _add_time_series_roms(self, ensemble_model: EnsembleModel, case: HeronCase,
416415
@ In, sources, list[Source], case sources
417416
@ Out, None
418417
"""
419-
dispatch_eval = self._template.find("DataObjects/DataSet[@name='dispatch_eval']") # type: DataSet
418+
dispatch_eval: DataSet = self._template.find("DataObjects/DataSet[@name='dispatch_eval']")
420419

421420
# Gather any ARMA sources from the list of sources
422421
arma_sources = [s for s in sources if s.is_type("ARMA")]
423422

424423
# Add cluster index info to dispatch variable groups and data objects
425424
if any(source.eval_mode == "clustered" for source in arma_sources):
426-
vg_dispatch = self._template.find("VariableGroups/Group[@name='GRO_dispatch']") # type: VariableGroup
425+
vg_dispatch: VariableGroup = self._template.find("VariableGroups/Group[@name='GRO_dispatch']")
427426
vg_dispatch.variables.append(self.namingTemplates["cluster_index"])
428427
dispatch_eval.add_index(self.namingTemplates["cluster_index"], "GRO_dispatch_in_Time")
429428

@@ -555,7 +554,7 @@ def _get_uncertain_cashflow_params(self,
555554
dist_name = self.namingTemplates["distribution"].format(variable=feat_name)
556555

557556
# Reconstruct distribution XML node from valuedParam definition
558-
dist_node = vp._vp.get_distribution() # type: ET.Element
557+
dist_node: ET.Element = vp._vp.get_distribution()
559558
dist_node.set("name", dist_name)
560559
dist_snippet = snippet_factory.from_xml(dist_node)
561560
distributions.append(dist_snippet)
@@ -597,7 +596,7 @@ def _create_sampler_variables(self,
597596
interaction = component.get_interaction()
598597
name = component.name
599598
var_name = self.namingTemplates["variable"].format(unit=name, feature="capacity")
600-
cap = interaction.get_capacity(None, raw=True) # type: ValuedParam
599+
cap: ValuedParam = interaction.get_capacity(None, raw=True)
601600

602601
if not cap.is_parametric(): # we already know the value
603602
continue
@@ -640,7 +639,7 @@ def _configure_static_history_sampler(self,
640639
if case.debug["enabled"]:
641640
indices.append(cluster_index)
642641

643-
time_series_vargroup = self._template.find("VariableGroups/Group[@name='GRO_timeseries']") # type: VariableGroup
642+
time_series_vargroup: VariableGroup = self._template.find("VariableGroups/Group[@name='GRO_timeseries']")
644643

645644
for source in filter(lambda x: x.is_type("CSV"), sources):
646645
# Add the source variables to the GRO_timeseries_in variable group

templates/snippets/outstreams.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,35 @@ def signals(self, value: list[str]) -> None:
155155
"""
156156
find_node(self, "signals").text = value
157157

158+
@property
159+
def sep_by_resource(self) -> bool:
160+
"""
161+
Getter for sep_by_resource node
162+
@ In, None
163+
@ Out, sep, bool, separate plots for each resource
164+
"""
165+
# "True" conditions
166+
# - node present and text is None
167+
# - node present and text is True
168+
# "False" conditions
169+
# - node not present
170+
# - node present and text is False
171+
node = self.find("sep_by_resource")
172+
# Need to be careful with falsy condition since bool(None) == False
173+
if node is None or (not bool(node.text) and node.text is not None):
174+
sep = False
175+
else:
176+
sep = True
177+
return sep
178+
179+
@sep_by_resource.setter
180+
def sep_by_resource(self, value: bool):
181+
"""
182+
Setting for sep_by_resource node
183+
@ In, value, bool, separate plots for each resource
184+
@ Out, None
185+
"""
186+
find_node(self, "sep_by_resource").text = value
158187

159188
class TealCashFlowPlot(OutStream):
160189
""" OutStream snippet for TEAL cashflow plots """

templates/template_driver.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,15 +88,14 @@ def write_workflow(self, dest_dir: str, case: HeronCase, components: list[Compon
8888
print("========================")
8989
print("HERON: writing files ...")
9090
print("========================")
91-
dest = self.template.get_write_path(dest_dir)
92-
self.template.writeWorkflow(self.template.template_xml, dest)
91+
self.template.writeWorkflow(dest_dir)
9392

9493
# Write library of info so it can be read in dispatch during inner run. Doing this here ensures that the lib file
9594
# is written just once, no matter the number of workflow files written by the template.
9695
lib_file = Path(dest_dir) / self.template.namingTemplates["lib file"]
9796
with lib_file.open("wb") as lib:
9897
pk.dump((case, components, sources), lib)
99-
print(f"Wrote '{lib_file.name}' to '{str(lib_file.resolve())}'")
98+
print(f"Wrote '{lib_file.name}' to '{str(lib_file)}'")
10099

101100
###################
102101
# Utility methods #

tests/integration_tests/mechanics/debug_mode/sweep/heron_input.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
<debug>
1616
<inner_samples>2</inner_samples>
1717
<macro_steps>2</macro_steps>
18-
<dispatch_plot>True</dispatch_plot>
18+
<dispatch_plot sep_by_resource="True">True</dispatch_plot>
1919
</debug>
2020
<num_arma_samples>1</num_arma_samples>
2121
<time_discretization>

tests/integration_tests/mechanics/npv_exempt/heron_input.xml

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
<author>dylanjm</author>
55
<created>2024-03-26</created>
66
<description>
7-
Tests NPV exempt Cashflows. This is a recreation of the Cashflows test from
7+
Tests NPV exempt Cashflows. This is a recreation of the Cashflows test from
88
the HERON test suite. The only difference is the Variable Operating and Maitenance
9-
Costs (VOM) cashflow is now NPV exempt. The Excel File 'analytic.xlsx' shows
10-
the expected results. When the NPV exemption is turned on, the mean NPV for
9+
Costs (VOM) cashflow is now NPV exempt. The Excel File 'analytic.xlsx' shows
10+
the expected results. When the NPV exemption is turned on, the mean NPV for
1111
both sweep values (1, 2) should be: $5,373.37 and $10,748.27 respectively.
12-
When NPV exemption is turned off (i.e. the Cashflows test) the results for
12+
When NPV exemption is turned off (i.e. the Cashflows test) the results for
1313
both sweep values should be $5,218.73 and $10,439.00 respectively. We can see
1414
that having the VOM cashflow NPV exempt increases the mean NPV in both cases
15-
since VOM is a cost and is left out of the NPV calculation.
15+
since VOM is a cost and is left out of the NPV calculation.
1616
</description>
1717
<classesTested>HERON</classesTested>
1818
</TestInfo>
@@ -62,6 +62,14 @@
6262
</scaling_factor_x>
6363
<!-- <depreciate>5</depreciate> -->
6464
</CashFlow>
65+
<CashFlow name="VOM" type="repeating" taxable="True" inflation="none" npv_exempt="True">
66+
<driver>
67+
<activity>a</activity>
68+
</driver>
69+
<reference_price>
70+
<fixed_value>-1</fixed_value>
71+
</reference_price>
72+
</CashFlow>
6573
<CashFlow name="FOM" type="repeating" period='year' taxable="False" inflation="none">
6674
<driver>
6775
<variable>source_capacity</variable>
@@ -77,14 +85,6 @@
7785
<fixed_value>0.999</fixed_value>
7886
</scaling_factor_x>
7987
</CashFlow>
80-
<CashFlow name="VOM" type="repeating" taxable="True" inflation="none" npv_exempt="True">
81-
<driver>
82-
<activity>a</activity>
83-
</driver>
84-
<reference_price>
85-
<fixed_value>-1</fixed_value>
86-
</reference_price>
87-
</CashFlow>
8888
</economics>
8989
</Component>
9090

0 commit comments

Comments
 (0)