From 44f44d22a4b683e198024f8c3cb6a70a6c52df30 Mon Sep 17 00:00:00 2001 From: Alper Altuntas Date: Tue, 21 May 2024 16:16:42 -0600 Subject: [PATCH] misc minor improvements in relational constraints, csp_solver, and mom6_Bathy. Also added a tool to detect violations due to combination of multiple reasons --- ProConPy/csp_solver.py | 10 +- external/mom6_bathy | 2 +- .../test_constraint_violation.py | 26 ++--- tools/find_combined_reasons.py | 99 +++++++++++++++++++ visualCaseGen/specs/relational_constraints.py | 19 ++-- 5 files changed, 128 insertions(+), 28 deletions(-) create mode 100644 tools/find_combined_reasons.py diff --git a/ProConPy/csp_solver.py b/ProConPy/csp_solver.py index 19f3f6e..a53b7fe 100644 --- a/ProConPy/csp_solver.py +++ b/ProConPy/csp_solver.py @@ -438,7 +438,15 @@ def retrieve_error_msg(self, var, new_value): ) error_messages = [str(err_msg) for err_msg in s.unsat_core()] - return f'Invalid assignment of {var} to {new_value}. Reason(s): {". ".join(error_messages)}' + msg = f'Invalid assignment of {var} to {new_value}.' + if len(error_messages) == 1: + msg += f' Reason: {error_messages[0]}' + else: + msg +=' Reasons:' + for i, err_msg in enumerate(error_messages): + msg += f' {i+1}: {err_msg}.' + msg = msg.replace('..', '.') + return msg def register_assignment(self, var, new_value): """Register the assignment of the given variable to the given value. The assignment is diff --git a/external/mom6_bathy b/external/mom6_bathy index a548fc4..ef5a8ab 160000 --- a/external/mom6_bathy +++ b/external/mom6_bathy @@ -1 +1 @@ -Subproject commit a548fc499e4e8d3f32a883087469f39f332400cd +Subproject commit ef5a8abe1ba13b2b60b753b4c1227755f577380b diff --git a/tests/2_integration/test_constraint_violation.py b/tests/2_integration/test_constraint_violation.py index 6bf3bbc..5b5067e 100755 --- a/tests/2_integration/test_constraint_violation.py +++ b/tests/2_integration/test_constraint_violation.py @@ -164,29 +164,19 @@ def test_multiple_reasons(): # Reset active stage Stage.active().reset() - + cvars['COMP_ATM'].value = "cam" cvars['COMP_ROF'].value = "drof" - cvars['COMP_GLC'].value = "cism" - - # Combination of three reasons - with pytest.raises(ConstraintViolation) as exc_info: - cvars['COMP_ATM'].value = "cam" - err_msg = str(exc_info.value) - assert "Data land model cannot be coupled with CAM." in err_msg - assert "CLM cannot be coupled with a data runoff model." in err_msg - assert "GLC cannot be coupled with a stub land model." in err_msg + cvars['COMP_WAV'].value = "dwav" - # Combination of six reasons + # Combination of five reasons with pytest.raises(ConstraintViolation) as exc_info: - cvars['COMP_OCN'].value = "mom" + cvars['COMP_GLC'].value = "cism" err_msg = str(exc_info.value) - assert "Data land model cannot be coupled with CAM." in err_msg - assert "CLM cannot be coupled with a data runoff model." in err_msg - assert "GLC cannot be coupled with a stub land model." in err_msg + assert "CLM cannot be coupled with a data runoff model" in err_msg assert "GLC, ROF, and WAV cannot be coupled with SLIM." in err_msg - assert "An active or data atmosphere model is needed to force ocean, ice, and/or runoff models." in err_msg - assert "When MOM|POP is coupled with data atmosphere (datm), LND component must be stub (slnd)." in err_msg - + assert "MOM6 cannot be coupled with data wave component" in err_msg + assert "GLC cannot be coupled with a stub land model, unless it is coupled with MOM6" in err_msg + assert "CAM-DLND coupling is not supported" in err_msg if __name__ == "__main__": test_constraint_violation_detection() diff --git a/tools/find_combined_reasons.py b/tools/find_combined_reasons.py new file mode 100644 index 0000000..fc5bbb2 --- /dev/null +++ b/tools/find_combined_reasons.py @@ -0,0 +1,99 @@ +"""Module to find combined reasons leading to a violation of constraints in visualCaseGen.""" + +from ProConPy.config_var import ConfigVar, cvars +from ProConPy.stage import Stage +from ProConPy.dev_utils import ConstraintViolation +from ProConPy.csp_solver import csp +from visualCaseGen.cime_interface import CIME_interface +from visualCaseGen.initialize_configvars import initialize_configvars +from visualCaseGen.initialize_widgets import initialize_widgets +from visualCaseGen.initialize_stages import initialize_stages +from visualCaseGen.specs.options import set_options +from visualCaseGen.specs.relational_constraints import get_relational_constraints + +import random + + +def initialize(cime): + """Initializes visualCaseGen""" + ConfigVar.reboot() + Stage.reboot() + initialize_configvars(cime) + initialize_widgets(cime) + initialize_stages(cime) + set_options(cime) + csp.initialize(cvars, get_relational_constraints(cvars), Stage.first()) + + +def set_preliminary_vars(): + """Sets the preliminary variables to move on to the Components stage""" + cvars["COMPSET_MODE"].value = "Custom" + cvars["INITTIME"].value = "2000" + assert Stage.active().title.startswith("Components") + + +def valid_options(var): + """Returns the valid options for a ConfigVar""" + return [opt for opt in var.options if var._options_validities[opt] is True] + + +def main(ntrial=10, nselect=5, minreason=3): + """Main function for the script. It generates random component selections and checks for + combined reasons leading to a violation. If a selection leads to a violation of a minimum + number of combined reasons (constraints), the selection histoary and the constraints are printed. + + Parameters + ---------- + ntrial : int + Number of trials to run + nselect : int + Number of selections to make in each trial + minreason : int + Minimum number of reasons leading to a violation to print the error message + """ + + cime = CIME_interface() + initialize(cime) + set_preliminary_vars() + comps = [f"COMP_{cc}" for cc in cime.comp_classes] + comps = [ + "COMP_ATM", + "COMP_LND", + "COMP_ICE", + "COMP_OCN", + "COMP_ROF", + "COMP_GLC", + "COMP_WAV", + ] + comps = ["COMP_ATM", "COMP_LND", "COMP_ICE", "COMP_OCN", "COMP_ROF", "COMP_WAV"] + + for i in range(ntrial): + print(f"Trial {i+1}/{ntrial}") + Stage.active().reset() + hist = [] + for s in range(nselect): + comp = random.choice(comps) + new_value = random.choice(valid_options(cvars[comp])) + # remove past assignment if exists: + hist = [h for h in hist if h[0] != comp] + hist.append((comp, new_value)) + if cvars[comp].value != new_value: + cvars[comp].value = new_value + + for comp_other in set(comps) - set([comp]): + var = cvars[comp_other] + for option in var.options: + if var._options_validities[option] is False: + err_msg = csp.retrieve_error_msg(var, option) + nreason = ( + err_msg.count(".") - 1 + ) # number of reasons leading to the violation + if nreason >= minreason: + print("------------------------------------------------") + print(f"Error message for {var.name} = {option}:") + print(err_msg) + print(f"Hist: {hist}") + + +if __name__ == "__main__": + main() diff --git a/visualCaseGen/specs/relational_constraints.py b/visualCaseGen/specs/relational_constraints.py index fc8cb80..8359508 100644 --- a/visualCaseGen/specs/relational_constraints.py +++ b/visualCaseGen/specs/relational_constraints.py @@ -51,10 +51,10 @@ def get_relational_constraints(cvars): "If CLM is coupled with DATM, then both ICE and OCN must be stub.", Implies(COMP_ATM=="satm", And(COMP_ICE=="sice", COMP_ROF=="srof", COMP_OCN=="socn")) : - "An active or data atmosphere model is needed to force ocean, ice, and/or runoff models.", + "An active/data atmosphere is needed to force ocean, ice, and/or runoff models.", - Implies(COMP_LND=="slnd", COMP_GLC=="sglc") : - "GLC cannot be coupled with a stub land model.", + Implies(COMP_LND=="slnd", Or(COMP_OCN=="mom", COMP_GLC=="sglc")) : + "GLC cannot be coupled with a stub land model, unless it is coupled with MOM6.", Implies(COMP_LND=="slim", And(COMP_GLC=="sglc", COMP_ROF=="srof", COMP_WAV=="swav")) : "GLC, ROF, and WAV cannot be coupled with SLIM.", @@ -66,7 +66,7 @@ def get_relational_constraints(cvars): "CLM cannot be coupled with a data runoff model.", Implies(COMP_LND=="dlnd", COMP_ATM!="cam") : # TODO: check this constraint. - "Data land model cannot be coupled with CAM.", + "CAM-DLND coupling is not supported.", Implies(COMP_OCN=="docn", COMP_OCN_OPTION != "(none)"): "Must pick a valid DOCN option.", @@ -115,8 +115,8 @@ def get_relational_constraints(cvars): "Core2 forcing can only be used with T62 grid.", # mom6_bathy-related constraints ------------------ - Implies(And(COMP_OCN=="mom", COMP_LND=="slnd", COMP_ICE=="sice"), OCN_LENY<=179.0): - "MOM6 grid cannot reach the poles when coupled with stub land and ice components.", + Implies(And(COMP_OCN=="mom", COMP_LND=="slnd", COMP_ICE=="sice"), OCN_LENY<180.0): + "If LND and ICE are stub, custom MOM6 grid must exclude poles (singularity).", Implies(And(COMP_OCN != "mom", COMP_LND!="clm"), GRID_MODE=="Standard"): "Custom grids can only be generated when MOM6 and/or CLM are selected.", @@ -148,8 +148,11 @@ def get_relational_constraints(cvars): Implies(OCN_GRID_EXTENT=="Global", OCN_LENX==360.0): "Global ocean model domains must have a length of 360 degrees in the x-direction.", - Implies(OCN_GRID_EXTENT=="Global", And(OCN_LENY>0.0, OCN_LENY<=180.0) ): - "OCN grid length in Y direction must be <= 180.0 when OCN grid extent is global.", + And(OCN_LENY>0.0, OCN_LENY<=180.0): + "OCN grid length in Y direction must be <= 180.0.", + + And(OCN_LENX>0.0, OCN_LENX<=360.0): + "OCN grid length in X direction must be <= 360.0.", # Custom lnd grid constraints ------------------ Implies(COMP_LND!="clm", LND_GRID_MODE=="Standard"):