Skip to content

Commit b4685ac

Browse files
Merge pull request #1124 from linsword13/inmem-fom
Extend to support in memory FOMs
2 parents 32fbc17 + c04f72c commit b4685ac

File tree

6 files changed

+134
-44
lines changed

6 files changed

+134
-44
lines changed

lib/ramble/ramble/language/shared_language.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,14 @@ def _execute_figure_of_merit_context(obj):
9898
@shared_directive("figures_of_merit")
9999
def figure_of_merit(
100100
name,
101-
fom_regex,
102-
group_name,
101+
fom_regex=None,
102+
group_name=None,
103103
log_file="{log_file}",
104104
units="",
105105
contexts=None,
106106
fom_type: FomType = FomType.UNDEFINED,
107107
when=None,
108+
fom_map_key=None,
108109
**kwargs,
109110
):
110111
"""Adds a figure of merit to track for this object
@@ -122,9 +123,16 @@ def figure_of_merit(
122123
should exist in.
123124
fom_type (ramble.util.foms.FomType): The type of figure of merit
124125
when (list | None): List of when conditions to apply to directive
126+
fom_map_key: If supplied, this is treated as an in-memory (as opposed to file-based)
127+
figure of merit, and its value is extracted using this key
125128
"""
126129

127130
def _execute_figure_of_merit(obj):
131+
if fom_map_key is None:
132+
if fom_regex is None or group_name is None:
133+
raise ramble.language.language_base.DirectiveError(
134+
"`fom_regex` and `group_name` are required for defining file-based FOM"
135+
)
128136
when_list = ramble.language.language_helpers.build_when_list(
129137
when, obj, name, "figure_of_merit"
130138
)
@@ -146,6 +154,7 @@ def _execute_figure_of_merit(obj):
146154
"fom_type": fom_type,
147155
"when": when_list,
148156
"origin_type": obj.origin_type if hasattr(obj, "origin_type") else "",
157+
"fom_map_key": fom_map_key,
149158
}
150159

151160
return _execute_figure_of_merit

lib/ramble/ramble/pipeline.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -634,7 +634,7 @@ def print_archive_files(app_inst, pattern_title, patterns):
634634
logger.all_msg(f"Experiment: {exp}")
635635
logger.all_msg(f" Experiment log file: {log_file}")
636636

637-
analysis_logs, _ = app_inst._analysis_dicts(self.workspace.success_list)
637+
analysis_logs, _, _ = app_inst._analysis_dicts(self.workspace.success_list)
638638

639639
logger.all_msg(" Auxiliary experiment logs:")
640640
for log in analysis_logs:

lib/ramble/ramble/test/application_language.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import deprecation
1111
import pytest
1212

13+
from ramble import language
1314
from ramble.appkit import * # noqa
1415

1516
app_types = [
@@ -280,6 +281,25 @@ def test_figure_of_merit_directive(app_class):
280281
assert app_inst.figures_of_merit[_FS][_FS][fom_name][conf_name] == conf_val
281282

282283

284+
def test_figure_of_merit_directive_required_args():
285+
app_inst = ExecutableApplication("/not/a/path") # noqa: F405
286+
with pytest.raises(
287+
language.language_base.DirectiveError, match="required for defining file-based FOM"
288+
):
289+
app_inst.figure_of_merit(
290+
"test_fom",
291+
units="s",
292+
)
293+
app_inst.figure_of_merit(
294+
"test_inmem_fom",
295+
units="s",
296+
fom_map_key="test_fom_map_key",
297+
)
298+
foms = list(list(app_inst.figures_of_merit.values())[0].values())[0]
299+
assert len(foms) == 1
300+
assert foms["test_inmem_fom"]["fom_map_key"] == "test_fom_map_key"
301+
302+
283303
@pytest.mark.parametrize("app_class", app_types)
284304
def test_input_file_directive(app_class):
285305
test_defs = {}

lib/ramble/ramble/test/end_to_end/expanded_fom_dry_run.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def test_expanded_foms_dry_run(
2929
mpi_command: 'mpirun -n {n_ranks} -ppn {processes_per_node}'
3030
batch_submit: 'batch_submit {execute_experiment}'
3131
n_threads: '1'
32+
my_var: 'testvar'
3233
applications:
3334
expanded_foms:
3435
workloads:
@@ -78,5 +79,7 @@ def test_expanded_foms_dry_run(
7879
text_results_files = glob.glob(os.path.join(ws1.root, "results*.txt"))
7980
with open(text_results_files[0]) as f:
8081
data = f.read()
82+
assert "Status = SUCCESS" in data
8183
for expected in expected_expansions:
8284
assert f"test_fom {expected} = 567.8 {unit}" in data
85+
assert "test_inmem_testvar = inmem_val" in data

var/ramble/repos/builtin.mock/applications/expanded_foms/application.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,20 @@ class ExpandedFoms(ExecutableApplication):
3737
units="{unit}",
3838
)
3939

40+
figure_of_merit(
41+
"test_inmem_{my_var}",
42+
units="",
43+
fom_map_key="test_inmem",
44+
)
45+
4046
success_criteria("Run", mode="string", match=r"Collect", file="{log_file}")
47+
48+
success_criteria(
49+
"Inmem FOM matched",
50+
mode="fom_comparison",
51+
fom_name="test_inmem_{my_var}",
52+
formula="{value} == 'inmem_val'",
53+
)
54+
55+
def _prepare_analysis(self, workspace, app_inst):
56+
self.add_inmem_fom_value("test_inmem", "inmem_val")

var/ramble/repos/builtin/base_classes/application-base/base_class.py

Lines changed: 83 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,8 @@ def __init__(self, file_path):
196196
self._input_lock = None
197197
self._software_lock = None
198198
self._experiment_graph = None
199+
# A dict storing fom values, currently it only stores inmem FOMs
200+
self._fom_map = {}
199201

200202
# Ensure we always have the application name, and this is never empty
201203
self.license_names = self.license_names + [self.name]
@@ -1427,12 +1429,8 @@ def _define_commands(self, exec_graph, success_list=None):
14271429
)
14281430
logs.append(expanded_log)
14291431

1430-
analysis_logs, _ = self._analysis_dicts(success_list)
1431-
1432-
for log in analysis_logs:
1433-
logs.append(log)
1434-
1435-
logs = list(dict.fromkeys(logs))
1432+
analysis_logs, _, _ = self._analysis_dicts(success_list)
1433+
logs = list(set(logs) | analysis_logs.keys())
14361434

14371435
for log in logs:
14381436
self._command_list.append('rm -f "%s"' % log)
@@ -2156,7 +2154,7 @@ def _archive_experiments(self, workspace, app_inst=None):
21562154

21572155
# Copy all figure of merit files
21582156
criteria_list = self.success_list
2159-
analysis_files, _ = self._analysis_dicts(criteria_list)
2157+
analysis_files, _, _ = self._analysis_dicts(criteria_list)
21602158
for file in analysis_files.keys():
21612159
if os.path.exists(file):
21622160
shutil.copy(file, archive_experiment_dir)
@@ -2201,6 +2199,30 @@ def _prepare_analysis(self, workspace, app_inst=None):
22012199
"""
22022200
pass
22032201

2202+
def _extract_inmem_foms(self, inmem_fom_defs, fom_values):
2203+
"""Extract in-memory FOMs"""
2204+
for context, foms in inmem_fom_defs.items():
2205+
if context not in fom_values:
2206+
fom_values[context] = {}
2207+
foms = inmem_fom_defs[context]["foms"]
2208+
for fom in foms:
2209+
fom_conf = inmem_fom_defs[context]["foms"][fom]
2210+
# Currently inmem FOM does not have semantics for expanded vars,
2211+
# so use the already expanded name and unit
2212+
fom_name = fom_conf["fom_name_expanded"]
2213+
# TODO: this can be extended to support derived FOMs,
2214+
# since the `fom_values` contains resolved file-based FOMs
2215+
fom_map_key = fom_conf["fom_map_key"]
2216+
fom_value = self._fom_map.get(fom_map_key)
2217+
expanded_fom_value = self.expander.expand_var(fom_value)
2218+
fom_values[context][fom_name] = {
2219+
"value": expanded_fom_value,
2220+
"units": fom_conf["units_expanded"],
2221+
"origin": fom_conf["origin"],
2222+
"origin_type": fom_conf["origin_type"],
2223+
"fom_type": fom_conf["fom_type"],
2224+
}
2225+
22042226
register_phase(
22052227
"analyze_experiments",
22062228
pipeline="analyze",
@@ -2247,17 +2269,16 @@ def format_context(context_match, context_format):
22472269
context_string = context_format.format(**context_val)
22482270
return context_string
22492271

2250-
fom_values = {}
2251-
22522272
criteria_list = self.success_list
22532273
if not criteria_list:
22542274
criteria_list = ramble.success_criteria.ScopedCriteriaList()
22552275
criteria_list.reset()
22562276

2257-
files, definitions = self._analysis_dicts(criteria_list)
2277+
files, f_defs, inmem_defs = self._analysis_dicts(criteria_list)
22582278

22592279
exp_lock = self.experiment_lock()
22602280

2281+
fom_values = {}
22612282
# Iterate over files. We already know they exist
22622283
with lk.ReadTransaction(exp_lock):
22632284
for file, file_conf in files.items():
@@ -2292,9 +2313,7 @@ def format_context(context_match, context_format):
22922313
# Iterate over contexts and add matched contexts to active_contexts
22932314
for context, foms in file_conf["contexts"].items():
22942315
if not context == _NULL_CONTEXT:
2295-
context_conf = definitions[context][
2296-
"definition"
2297-
]
2316+
context_conf = f_defs[context]["definition"]
22982317
context_match = context_conf["regex"].match(
22992318
line
23002319
)
@@ -2314,7 +2333,7 @@ def format_context(context_match, context_format):
23142333
fom_values[context_name] = {}
23152334

23162335
for fom in foms:
2317-
fom_conf = definitions[context]["foms"][fom]
2336+
fom_conf = f_defs[context]["foms"][fom]
23182337
fom_match = fom_conf["regex"].match(line)
23192338

23202339
if fom_match:
@@ -2389,6 +2408,7 @@ def format_context(context_match, context_format):
23892408
"fom_type"
23902409
],
23912410
}
2411+
self._extract_inmem_foms(inmem_defs, fom_values)
23922412

23932413
# Test all non-file based success criteria
23942414
for criteria_obj in criteria_list.all_criteria():
@@ -2397,7 +2417,7 @@ def format_context(context_match, context_format):
23972417
criteria_obj.mark_found()
23982418

23992419
# If an app has no FOMs defined, don't fail it for that
2400-
success = (not definitions) or False
2420+
success = (not f_defs and not inmem_defs) or False
24012421
for fom in fom_values.values():
24022422
for value in fom.values():
24032423
if (
@@ -2733,12 +2753,13 @@ def _analysis_dicts(self, criteria_list):
27332753
27342754
Returns:
27352755
files (dict): All files that need to be processed
2736-
contexts (dict): Any contexts that have been defined
2737-
foms (dict): All figures of merit that need to be extracted
2756+
file_fom_defs (dict): Definitions of all file-backed FOMs to be extracted
2757+
inmem_fom_defs (dict): Definitions of all in-memory FOMs to be extracted
27382758
"""
27392759

27402760
files = {}
2741-
definitions = {}
2761+
file_fom_defs = {}
2762+
inmem_fom_defs = {}
27422763

27432764
# Add the application defined criteria
27442765
criteria_list.flush_scope("application_definition")
@@ -2861,32 +2882,38 @@ def _analysis_dicts(self, criteria_list):
28612882
"definitions and 'when' conditions."
28622883
)
28632884

2864-
# Copy context definition for contexts used by a FOM
2865-
if context not in definitions:
2866-
definitions[context] = {
2867-
"definition": {},
2868-
"foms": {},
2869-
}
2870-
if context != _NULL_CONTEXT:
2871-
regex_str = self.expander.expand_var(
2872-
all_contexts[context]["regex"]
2873-
)
2874-
definitions[context]["definition"] = {
2875-
"regex": re.compile(regex_str),
2876-
"format": all_contexts[context][
2877-
"output_format"
2878-
],
2885+
def _preset_context_dict(dest_def_dict, context):
2886+
# Copy context definition for contexts used by a FOM
2887+
if context not in dest_def_dict:
2888+
dest_def_dict[context] = {
2889+
"definition": {},
2890+
"foms": {},
28792891
}
2892+
if context != _NULL_CONTEXT:
2893+
regex_str = self.expander.expand_var(
2894+
all_contexts[context]["regex"]
2895+
)
2896+
dest_def_dict[context]["definition"] = {
2897+
"regex": re.compile(regex_str),
2898+
"format": all_contexts[context][
2899+
"output_format"
2900+
],
2901+
}
28802902

28812903
for fom, source_def in source_foms.items():
2882-
if fom in definitions[context]["foms"]:
2904+
is_inmem = source_def["fom_map_key"] is not None
2905+
dest_def_dict = (
2906+
inmem_fom_defs if is_inmem else file_fom_defs
2907+
)
2908+
_preset_context_dict(dest_def_dict, context)
2909+
if fom in dest_def_dict[context]["foms"]:
28832910
logger.warn(
28842911
f"FOM {fom} already defined in context {context} by "
2885-
f"{definitions[context]['foms'][fom]['origin']}. "
2912+
f"{dest_def_dict[context]['foms'][fom]['origin']}. "
28862913
f"Overwriting with new definition from {source.name}"
28872914
)
28882915
else:
2889-
definitions[context]["foms"][fom] = {}
2916+
dest_def_dict[context]["foms"][fom] = {}
28902917

28912918
def _expand_var(var):
28922919
return self.expander.expand_var(
@@ -2905,12 +2932,21 @@ def _try_expand_var_or_none(var: str, expander):
29052932
"origin": source.name,
29062933
"origin_type": source.origin_type,
29072934
"contexts": set(source_def["contexts"]),
2908-
"group": _expand_var(source_def["group_name"]),
2935+
"group": (
2936+
""
2937+
if is_inmem
2938+
else _expand_var(source_def["group_name"])
2939+
),
29092940
"units": _expand_var(source_def["units"]),
2910-
"regex": re.compile(
2911-
_expand_var(source_def["regex"])
2941+
"regex": (
2942+
""
2943+
if is_inmem
2944+
else re.compile(
2945+
_expand_var(source_def["regex"])
2946+
)
29122947
),
29132948
"fom_type": source_def["fom_type"].to_dict(),
2949+
"fom_map_key": source_def["fom_map_key"],
29142950
# If expansion works (i.e., it doesn't rely on the matched fom
29152951
# groups), then cache it here to avoid repeated expansion later.
29162952
"units_expanded": _try_expand_var_or_none(
@@ -2921,8 +2957,10 @@ def _try_expand_var_or_none(var: str, expander):
29212957
),
29222958
}
29232959

2924-
definitions[context]["foms"][fom] = fom_def
2960+
dest_def_dict[context]["foms"][fom] = fom_def
29252961

2962+
if is_inmem:
2963+
continue
29262964
log_path = _expand_var(source_def["log_file"])
29272965
# Ensure log path is absolute. If not, prepend the experiment run dir
29282966
if (
@@ -2944,7 +2982,11 @@ def _try_expand_var_or_none(var: str, expander):
29442982
logger.debug("Log = %s" % log_path)
29452983
logger.debug("Conf = %s" % fom_def)
29462984

2947-
return files, definitions
2985+
return files, file_fom_defs, inmem_fom_defs
2986+
2987+
def add_inmem_fom_value(self, fom_map_key, value):
2988+
"""Add value to an in-memory FOM"""
2989+
self._fom_map[fom_map_key] = value
29482990

29492991
def read_status(self):
29502992
"""Read status from an experiment's status file, if possible.

0 commit comments

Comments
 (0)