Repository: ESMCI/cime

Issue:
Optional stub components not supported in compset name
While the ESP component type is optional, it is not handled in a uniform manner. In addition, there is no support for new optional components.
Stub components should be optional in a compset long name.


Discussion:

Resolving patch:
diff --git a/scripts/lib/CIME/case/case.py b/scripts/lib/CIME/case/case.py
--- a/scripts/lib/CIME/case/case.py
+++ b/scripts/lib/CIME/case/case.py
@@ -464,16 +464,19 @@ def _set_compset(self, compset_name, files, driver="mct"):
                     self._compsetname = match
                     logger.info("Compset longname is {}".format(match))
                     logger.info("Compset specification file is {}".format(compsets_filename))
-                    return compset_alias, science_support
+                    break
 
         if compset_alias is None:
             logger.info("Did not find an alias or longname compset match for {} ".format(compset_name))
             self._compsetname = compset_name
-            # if this is a valiid compset longname there will be at least 7 components.
-            components = self.get_compset_components()
-            expect(len(components) > 6, "No compset alias {} found and this does not appear to be a compset longname.".format(compset_name))
 
-        return None, science_support
+        # Fill in compset name
+        self._compsetname, self._components = self.valid_compset(self._compsetname, files)
+        # if this is a valiid compset longname there will be at least 7 components.
+        components = self.get_compset_components()
+        expect(len(components) > 6, "No compset alias {} found and this does not appear to be a compset longname.".format(compset_name))
+
+        return compset_alias, science_support
 
     def get_primary_component(self):
         if self._primary_component is None:
@@ -529,6 +532,96 @@ def _find_primary_component(self):
 
         return primary_component
 
+    def _valid_compset_impl(self, compset_name, comp_classes, comp_hash):
+        """Add stub models missing in <compset_name>, return full compset name.
+        <comp_classes> is a list of all supported component classes.
+        <comp_hash> is a dictionary where each key is a supported component
+        (e.g., datm) and the associated value is the index in <comp_classes> of
+        that component's class (e.g., 1 for atm).
+        >>> Case(read_only=False)._valid_compset_impl('2000_DATM%NYF_SLND_DICE%SSMI_DOCN%DOM_DROF%NYF_SGLC_SWAV', ['CPL', 'ATM', 'LND', 'ICE', 'OCN', 'ROF', 'GLC', 'WAV', 'ESP'], {'datm':1,'satm':1,'dlnd':2,'slnd':2,'dice':3,'sice':3,'docn':4,'socn':4,'drof':5,'srof':5,'sglc':6,'swav':7,'ww3':7,'sesp':8})
+        ('2000_DATM%NYF_SLND_DICE%SSMI_DOCN%DOM_DROF%NYF_SGLC_SWAV_SESP', ['2000', 'DATM%NYF', 'SLND', 'DICE%SSMI', 'DOCN%DOM', 'DROF%NYF', 'SGLC', 'SWAV', 'SESP'])
+        >>> Case(read_only=False)._valid_compset_impl('2000_DATM%NYF_DICE%SSMI_DOCN%DOM_DROF%NYF', ['CPL', 'ATM', 'LND', 'ICE', 'OCN', 'ROF', 'GLC', 'WAV', 'ESP'], {'datm':1,'satm':1,'dlnd':2,'slnd':2,'dice':3,'sice':3,'docn':4,'socn':4,'drof':5,'srof':5,'sglc':6,'swav':7,'ww3':7,'sesp':8})
+        ('2000_DATM%NYF_SLND_DICE%SSMI_DOCN%DOM_DROF%NYF_SGLC_SWAV_SESP', ['2000', 'DATM%NYF', 'SLND', 'DICE%SSMI', 'DOCN%DOM', 'DROF%NYF', 'SGLC', 'SWAV', 'SESP'])
+        >>> Case(read_only=False)._valid_compset_impl('2000_DICE%SSMI_DOCN%DOM_DATM%NYF_DROF%NYF', ['CPL', 'ATM', 'LND', 'ICE', 'OCN', 'ROF', 'GLC', 'WAV', 'ESP'], {'datm':1,'satm':1,'dlnd':2,'slnd':2,'dice':3,'sice':3,'docn':4,'socn':4,'drof':5,'srof':5,'sglc':6,'swav':7,'ww3':7,'sesp':8})
+        ('2000_DATM%NYF_SLND_DICE%SSMI_DOCN%DOM_DROF%NYF_SGLC_SWAV_SESP', ['2000', 'DATM%NYF', 'SLND', 'DICE%SSMI', 'DOCN%DOM', 'DROF%NYF', 'SGLC', 'SWAV', 'SESP'])
+        >>> Case(read_only=False)._valid_compset_impl('1850_CAM60_CLM50%BGC-CROP_CICE_POP2%ECO%ABIO-DIC_MOSART_CISM2%NOEVOLVE_WW3_BGC%BDRD', ['CPL', 'ATM', 'LND', 'ICE', 'OCN', 'ROF', 'GLC', 'WAV', 'ESP'], {'datm':1,'satm':1, 'cam':1,'dlnd':2,'clm':2,'slnd':2,'cice':3,'dice':3,'sice':3,'pop':4,'docn':4,'socn':4,'mosart':5,'drof':5,'srof':5,'cism':6,'sglc':6,'ww':7,'swav':7,'ww3':7,'sesp':8})
+        ('1850_CAM60_CLM50%BGC-CROP_CICE_POP2%ECO%ABIO-DIC_MOSART_CISM2%NOEVOLVE_WW3_SESP_BGC%BDRD', ['1850', 'CAM60', 'CLM50%BGC-CROP', 'CICE', 'POP2%ECO%ABIO-DIC', 'MOSART', 'CISM2%NOEVOLVE', 'WW3', 'SESP', 'BGC%BDRD'])
+        """
+        # Find the models declared in the compset
+        model_set = [None]*len(comp_classes)
+        components = compset_name.split('_')
+        model_set[0] = components[0]
+        # Check for BGC
+        if components[-1][0:3] == 'BGC':
+            bgc = components[-1]
+            last_ind = len(components) - 1
+        else:
+            bgc = None
+            last_ind = len(components)
+
+        for model in components[1:last_ind]:
+            match = Case.__mod_match_re__.match(model.lower())
+            expect(match is not None, "No model match for {}".format(model))
+            mod_match = match.group(1)
+            expect(mod_match in comp_hash,
+                   "Unknown model type, {}".format(model))
+            comp_ind = comp_hash[mod_match]
+            model_set[comp_ind] = model
+
+        # Fill in missing components with stubs
+        for comp_ind in range(1, len(model_set)):
+            if model_set[comp_ind] is None:
+                comp_class = comp_classes[comp_ind]
+                stub = 'S' + comp_class
+                logger.info("Automatically adding {} to compset".format(stub))
+                model_set[comp_ind] = stub
+
+        # Return the completed compset
+        if bgc is not None:
+            model_set.append(bgc)
+
+        compsetname = '_'.join(model_set)
+        return compsetname, model_set
+
+    # RE to match component type name without optional piece (stuff after %).
+    # Drop any trailing digits (e.g., the 60 in CAM60) to ensure match
+    # Note, this will also drop trailing digits such as in ww3 but since it
+    # is handled consistenly, this should not affect functionality.
+    # Note: interstitial digits are included (e.g., in FV3GFS).
+    __mod_match_re__ = re.compile(r"([^%]*[^0-9%]+)")
+    def valid_compset(self, compset_name, files):
+        """Add stub models missing in <compset_name>, return full compset name.
+        <files> is used to collect set of all supported components.
+        """
+        # First, create hash of model names
+        # A note about indexing. Relevant component classes start at 1
+        # because we ignore CPL for finding model components.
+        # Model components would normally start at zero but since we are
+        # dealing with a compset, 0 is reserved for the time field
+        drv_config_file = files.get_value("CONFIG_CPL_FILE")
+        drv_comp = Component(drv_config_file, "CPL")
+        comp_classes = drv_comp.get_valid_model_components()
+        comp_hash = {} # Hash model name to component class index
+        for comp_ind in range(1, len(comp_classes)):
+            comp = comp_classes[comp_ind]
+            # Find list of models for component class
+            # List can be in different locations, check CONFIG_XXX_FILE
+            node_name = 'CONFIG_{}_FILE'.format(comp)
+            models = files.get_components(node_name)
+            if (models is None) or (None in models):
+                # Backup, check COMP_ROOT_DIR_XXX
+                node_name = 'COMP_ROOT_DIR_' + comp
+                models = files.get_components(node_name)
+
+            expect((models is not None) and (None not in models),
+                   "Unable to find list of supported components")
+
+            for model in models:
+                mod_match = Case.__mod_match_re__.match(model.lower()).group(1)
+                comp_hash[mod_match] = comp_ind
+
+        return self._valid_compset_impl(compset_name, comp_classes, comp_hash)
+
 
     def _set_info_from_primary_component(self, files, pesfile=None):
         """
@@ -633,9 +726,6 @@ def _get_component_config_data(self, files, driver=None):
         # for each component
         self.set_comp_classes(drv_comp.get_valid_model_components())
 
-        if len(self._component_classes) > len(self._components):
-            self._components.append('sesp')
-
         # will need a change here for new cpl components
         root_dir_node_name = 'COMP_ROOT_DIR_CPL'
         comp_root_dir = files.get_value(root_dir_node_name, {"component":driver}, resolved=False)
@@ -788,6 +878,9 @@ def configure(self, compset_name, grid_name, machine_name=None,
         #--------------------------------------------
         files = Files(comp_interface=driver)
 
+        #--------------------------------------------
+        # find and/or fill out compset name
+        #--------------------------------------------
         compset_alias, science_support = self._set_compset(compset_name, files, driver)
 
         self._components = self.get_compset_components()


Test patch: