Skip to content

Commit 36cf5e4

Browse files
authored
Merge pull request #4759 from ESMCI/adds-query_config-compiler-info
Adds --compiler argument to query_config. This argument should be used when querying a specific machine e.g. --machines frontier. When specified, modules and environment_variables that match the compiler will be printed. Additional requirements e.g. DEBUG="TRUE" will be printed above sections that require them. Additionally for environment_variables there is an attempt to resolve any CIME specific templates e.g. $ENV{HOME}. Test suite: Test baseline: n/a Test namelist changes: n/a Test status: n/a Fixes #4743 User interface changes?: Update gh-pages html (Y/N)?:
2 parents 3fcb1d3 + fe49f5f commit 36cf5e4

File tree

3 files changed

+423
-82
lines changed

3 files changed

+423
-82
lines changed

CIME/XML/machines.py

+200-23
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,45 @@
11
"""
22
Interface to the config_machines.xml file. This class inherits from GenericXML.py
33
"""
4+
45
from CIME.XML.standard_module_setup import *
56
from CIME.XML.generic_xml import GenericXML
67
from CIME.XML.files import Files
7-
from CIME.utils import convert_to_unknown_type, get_cime_config
8+
from CIME.utils import CIMEError, expect, convert_to_unknown_type, get_cime_config
89

10+
import re
11+
import logging
912
import socket
13+
from functools import partial
1014
from pathlib import Path
1115

1216
logger = logging.getLogger(__name__)
1317

1418

19+
def match_value_by_attribute_regex(element, attribute_name, value):
20+
"""Checks element contains attribute whose pattern matches a value.
21+
22+
If the element does not have the attribute it's considered a match.
23+
24+
Args:
25+
element (CIME.XML.generic_xml._Element): XML element to check attributes.
26+
attribute_name (str): Name of attribute with regex value.
27+
value (str): Value that is matched against attributes regex value.
28+
29+
Returns:
30+
bool: True if attribute regex matches the target value otherwise False.
31+
"""
32+
attribute_value = element.attrib.get(attribute_name, None)
33+
34+
return (
35+
True
36+
if value is None
37+
or attribute_value is None
38+
or re.match(attribute_value, value) is not None
39+
else False
40+
)
41+
42+
1543
class Machines(GenericXML):
1644
def __init__(
1745
self,
@@ -484,30 +512,179 @@ def set_value(self, vid, value, subgroup=None, ignore_type=True):
484512
# A temporary cache only
485513
self.custom_settings[vid] = value
486514

487-
def print_values(self):
488-
# write out machines
489-
machines = self.get_children("machine")
490-
logger.info("Machines")
491-
for machine in machines:
492-
name = self.get(machine, "MACH")
493-
desc = self.get_child("DESC", root=machine)
494-
os_ = self.get_child("OS", root=machine)
495-
compilers = self.get_child("COMPILERS", root=machine)
496-
max_tasks_per_node = self.get_child("MAX_TASKS_PER_NODE", root=machine)
497-
max_mpitasks_per_node = self.get_child(
498-
"MAX_MPITASKS_PER_NODE", root=machine
515+
def print_values(self, compiler=None):
516+
"""Prints machine values.
517+
518+
Args:
519+
compiler (str, optional): Name of the compiler to print extra details for. Defaults to None.
520+
"""
521+
current = self.probe_machine_name(False)
522+
523+
if self.machine_node is None:
524+
for machine in self.get_children("machine"):
525+
self._print_machine_values(machine, current)
526+
else:
527+
self._print_machine_values(self.machine_node, current, compiler)
528+
529+
def _print_machine_values(self, machine, current=None, compiler=None):
530+
"""Prints a machines details.
531+
532+
Args:
533+
machine (CIME.XML.machines.Machine): Machine object.
534+
current (str, optional): Name of the current machine. Defaults to None.
535+
compiler (str, optional): If not None, then modules and environment variables matching compiler are printed. Defaults to None.
536+
537+
Raises:
538+
CIMEError: If `compiler` is not valid.
539+
"""
540+
name = self.get(machine, "MACH")
541+
if current is not None and current == name:
542+
name = f"{name} (current)"
543+
desc = self.text(self.get_child("DESC", root=machine))
544+
os_ = self.text(self.get_child("OS", root=machine))
545+
546+
compilers = self.text(self.get_child("COMPILERS", root=machine))
547+
if compiler is not None and compiler not in compilers.split(","):
548+
raise CIMEError(
549+
f"Compiler {compiler!r} is not a valid choice from ({compilers})"
499550
)
500-
max_gpus_per_node = self.get_child("MAX_GPUS_PER_NODE", root=machine)
501551

502-
print(" {} : {} ".format(name, self.text(desc)))
503-
print(" os ", self.text(os_))
504-
print(" compilers ", self.text(compilers))
505-
if max_mpitasks_per_node is not None:
506-
print(" pes/node ", self.text(max_mpitasks_per_node))
507-
if max_tasks_per_node is not None:
508-
print(" max_tasks/node ", self.text(max_tasks_per_node))
509-
if max_gpus_per_node is not None:
510-
print(" max_gpus/node ", self.text(max_gpus_per_node))
552+
mpilibs_nodes = self._get_children_filter_attribute_regex(
553+
"MPILIBS", "compiler", compiler, root=machine
554+
)
555+
mpilibs = set([y for x in mpilibs_nodes for y in self.text(x).split(",")])
556+
557+
max_tasks_per_node = self.text(
558+
self.get_child("MAX_TASKS_PER_NODE", root=machine)
559+
)
560+
max_mpitasks_per_node = self.text(
561+
self.get_child("MAX_MPITASKS_PER_NODE", root=machine)
562+
)
563+
max_gpus_per_node = self.get_optional_child("MAX_GPUS_PER_NODE", root=machine)
564+
max_gpus_per_node_text = (
565+
self.text(max_gpus_per_node) if max_gpus_per_node else 0
566+
)
567+
568+
if compiler is not None:
569+
name = f"{name} ({compiler})"
570+
571+
print(" {} : {} ".format(name, desc))
572+
print(" os ", os_)
573+
print(" compilers ", compilers)
574+
print(" mpilibs ", ",".join(mpilibs))
575+
print(" pes/node ", max_mpitasks_per_node)
576+
print(" max_tasks/node ", max_tasks_per_node)
577+
print(" max_gpus/node ", max_gpus_per_node_text)
578+
print("")
579+
580+
if compiler is not None:
581+
module_system_node = self.get_child("module_system", root=machine)
582+
583+
def command_formatter(node):
584+
if node.text is None:
585+
return f"{node.attrib['name']}"
586+
else:
587+
return f"{node.attrib['name']} {node.text}"
588+
589+
print(" Module commands:")
590+
for requirements, commands in self._filter_children_by_compiler(
591+
"modules", "command", compiler, command_formatter, module_system_node
592+
):
593+
indent = "" if requirements == "" else " "
594+
if requirements != "":
595+
print(f" (with {requirements})")
596+
for x in commands:
597+
print(f" {indent}{x}")
598+
print("")
599+
600+
def env_formatter(node, machines=None):
601+
return f"{node.attrib['name']}: {machines._get_resolved_environment_variable(node.text)}"
602+
603+
print(" Environment variables:")
604+
for requirements, variables in self._filter_children_by_compiler(
605+
"environment_variables",
606+
"env",
607+
compiler,
608+
partial(env_formatter, machines=self),
609+
machine,
610+
):
611+
indent = "" if requirements == "" else " "
612+
if requirements != "":
613+
print(f" (with {requirements})")
614+
for x in variables:
615+
print(f" {indent}{x}")
616+
617+
def _filter_children_by_compiler(self, parent, child, compiler, formatter, root):
618+
"""Filters parent nodes and returns requirements and children of filtered nodes.
619+
620+
Example of a yielded values:
621+
622+
"mpilib=openmpi DEBUG=true", ["HOME: /home/dev", "NETCDF_C_PATH: ../netcdf"]
623+
624+
Args:
625+
parent (str): Name of the nodes to filter.
626+
child (str): Name of the children nodes from filtered parent nodes.
627+
compiler (str): Name of the compiler that will be matched against the regex.
628+
formatter (function): Function to format the child nodes from the parents that match.
629+
root (CIME.XML.generic_xml._Element): Root node to filter parent nodes from.
630+
631+
Yields:
632+
str, list: Requirements for parent node and list of formated child nodes.
633+
"""
634+
nodes = self._get_children_filter_attribute_regex(
635+
parent, "compiler", compiler, root=root
636+
)
637+
638+
for x in nodes:
639+
attrib = {**x.attrib}
640+
attrib.pop("compiler", None)
641+
642+
requirements = " ".join([f"{y}={z!r}" for y, z in attrib.items()])
643+
values = [formatter(y) for y in self.get_children(child, root=x)]
644+
645+
yield requirements, values
646+
647+
def _get_children_filter_attribute_regex(self, name, attribute_name, value, root):
648+
"""Filter children nodes using regex.
649+
650+
Uses regex from attribute of children nodes to match a value.
651+
652+
Args:
653+
name (str): Name of the children nodes.
654+
attribute_name (str): Name of the attribute on the child nodes to build regex from.
655+
value (str): Value that is matched using regex from attribute.
656+
root (CIME.XML.generic_xml._Element): Root node to query children nodes from.
657+
658+
Returns:
659+
list: List of children whose regex attribute matches the value.
660+
"""
661+
return [
662+
x
663+
for x in self.get_children(name, root=root)
664+
if match_value_by_attribute_regex(x, attribute_name, value)
665+
]
666+
667+
def _get_resolved_environment_variable(self, text):
668+
"""Attempts to resolve machines environment variable.
669+
670+
Args:
671+
text (str): Environment variable value.
672+
673+
Returns:
674+
str: Resolved value or error message.
675+
"""
676+
if text is None:
677+
return ""
678+
679+
try:
680+
value = self.get_resolved_value(text, allow_unresolved_envvars=True)
681+
except Exception as e:
682+
return f"Failed to resolve {text!r} with: {e!s}"
683+
684+
if value == text and "$" in text:
685+
value = f"Failed to resolve {text!r}"
686+
687+
return value
511688

512689
def return_values(self):
513690
"""return a dictionary of machine info

CIME/scripts/query_config.py

+40-58
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,11 @@ def parse_command_line(description):
9696
help="Query machines for model. If not value is passed, all machines will be printed.",
9797
)
9898

99+
config_group.add_argument(
100+
"--compiler",
101+
help="Prints compiler details when combined with --machines",
102+
)
103+
99104
output_group = parser.add_argument_group("Output options")
100105

101106
output_group.add_argument(
@@ -133,6 +138,18 @@ def parse_command_line(description):
133138
if not any([kwargs[x] for x in ["grids", "compsets", "components", "machines"]]):
134139
parser.print_help(sys.stderr)
135140

141+
if kwargs["compiler"] is not None and (
142+
kwargs["machines"] is None or kwargs["machines"] == "all"
143+
):
144+
parser.print_help(sys.stderr)
145+
146+
print("")
147+
print(
148+
"The --compiler argument must be used when specifying a machine with --machines <name>"
149+
)
150+
151+
sys.exit(1)
152+
136153
kwargs["files"] = files[kwargs["driver"]]
137154

138155
return kwargs
@@ -313,7 +330,7 @@ def _query_component_settings(component, files, xml=False, all_components=False,
313330
component.print_values()
314331

315332

316-
def query_machines(files, machines, xml, **_):
333+
def query_machines(files, machines, xml, compiler, **_):
317334
config_file = files.get_value("MACHINES_SPEC_FILE")
318335
utils.expect(
319336
os.path.isfile(config_file),
@@ -334,68 +351,33 @@ def query_machines(files, machines, xml, **_):
334351
)
335352
)
336353
else:
337-
print_machine_values(xml_machines, machines)
354+
print_machine_values(xml_machines, compiler, machines)
338355

339356

340357
def print_machine_values(
341-
machine, machine_name="all"
358+
machine,
359+
compiler,
360+
machine_name="all",
342361
): # pylint: disable=arguments-differ
343-
# set flag to look for single machine
344-
if "all" not in machine_name:
345-
single_machine = True
346-
if machine_name == "current":
347-
machine_name = machine.probe_machine_name(warn=False)
362+
"""Prints machine values
363+
364+
Args:
365+
machine (CIME.XML.machines.Machine): Machine object.
366+
machine_name (str, optional): Which machine to print values for, can be "all", "current", or specific name. Defaults to "all".
367+
"""
368+
if machine_name == "current":
369+
machine_name = machine.probe_machine_name(False)
370+
371+
if machine_name == "all":
372+
machine_names = machine.list_available_machines()
348373
else:
349-
single_machine = False
350-
351-
# if we can't find the specified machine
352-
if single_machine and machine_name is None:
353-
files = Files()
354-
config_file = files.get_value("MACHINES_SPEC_FILE")
355-
print("Machine is not listed in config file: {}".format(config_file))
356-
else: # write out machines
357-
if single_machine:
358-
machine_names = [machine_name]
359-
else:
360-
machine_names = machine.list_available_machines()
361-
print("Machine(s)\n")
362-
for name in machine_names:
363-
machine.set_machine(name)
364-
desc = machine.text(machine.get_child("DESC"))
365-
os_ = machine.text(machine.get_child("OS"))
366-
compilers = machine.text(machine.get_child("COMPILERS"))
367-
mpilibnodes = machine.get_children("MPILIBS", root=machine.machine_node)
368-
mpilibs = []
369-
for node in mpilibnodes:
370-
mpilibs.extend(machine.text(node).split(","))
371-
# This does not include the possible depedancy of mpilib on compiler
372-
# it simply provides a list of mpilibs available on the machine
373-
mpilibs = list(set(mpilibs))
374-
max_tasks_per_node = machine.text(machine.get_child("MAX_TASKS_PER_NODE"))
375-
mpitasks_node = machine.get_optional_child(
376-
"MAX_MPITASKS_PER_NODE", root=machine.machine_node
377-
)
378-
max_mpitasks_per_node = (
379-
machine.text(mpitasks_node) if mpitasks_node else max_tasks_per_node
380-
)
381-
max_gpus_node = machine.get_optional_child(
382-
"MAX_GPUS_PER_NODE", root=machine.machine_node
383-
)
384-
max_gpus_per_node = machine.text(max_gpus_node) if max_gpus_node else 0
385-
386-
current_machine = machine.probe_machine_name(warn=False)
387-
name += " (current)" if current_machine and current_machine in name else ""
388-
print(" {} : {} ".format(name, desc))
389-
print(" os ", os_)
390-
print(" compilers ", compilers)
391-
print(" mpilibs ", mpilibs)
392-
if max_mpitasks_per_node is not None:
393-
print(" pes/node ", max_mpitasks_per_node)
394-
if max_tasks_per_node is not None:
395-
print(" max_tasks/node ", max_tasks_per_node)
396-
if max_gpus_per_node is not None:
397-
print(" max_gpus/node ", max_gpus_per_node)
398-
print("")
374+
machine_names = [machine_name]
375+
376+
print("Machine(s)\n")
377+
378+
for name in machine_names:
379+
machine.set_machine(name)
380+
machine.print_values(compiler)
399381

400382

401383
if __name__ == "__main__":

0 commit comments

Comments
 (0)