From b27a8e27f32981abe71630d7aec8ee9161069bbc Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Mon, 27 Nov 2023 12:08:17 +0000 Subject: [PATCH 01/62] refactor to isolate retrieving full json dict of moose objs --- catbird/cbird.py | 60 +++++++++++++++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 21 deletions(-) diff --git a/catbird/cbird.py b/catbird/cbird.py index 4d86d9d..a698001 100644 --- a/catbird/cbird.py +++ b/catbird/cbird.py @@ -114,7 +114,42 @@ def to_node(self): return node -def app_from_json(json_file, problem_names=None): +def json_from_exec(exec): + """ + Returns the Python objects corresponding to the MOOSE application described + by the json file. + + Parameters + ---------- + json_file : str, or Path + Either an open file handle, or a path to the json file. If `json` is a + dict, it is assumed this is a pre-parsed json object. + + Returns + ------- + dict + A dictionary of all MOOSE objects + """ + json_proc = subprocess.Popen([exec, '--json'], stdout=subprocess.PIPE) + json_str = '' + + # filter out the header and footer from the json data + while True: + line = json_proc.stdout.readline().decode() + if not line: + break + if '**START JSON DATA**' in line: + continue + if '**END JSON DATA**' in line: + continue + + json_str += line + + j_obj = json.loads(json_str) + + return j_obj + +def problems_from_json(json_file, problem_names=None): """ Returns the Python objects corresponding to the MOOSE application described by the json file. @@ -206,15 +241,13 @@ def parse_problems(json_obj, problem_names=None): return instances_out -def app_from_exec(exec, problem_names=None): +def problem_from_exec(exec, problem_names=None): """ Returns the Python objects corresponding to the MOOSE application described by the json file. Parameters ---------- - json : str or Path - Path to the MOOSE executable problems : Iterable of str Set of problems to generate classes for @@ -224,21 +257,6 @@ def app_from_exec(exec, problem_names=None): A dictionary of problem objects """ - json_proc = subprocess.Popen([exec, '--json'], stdout=subprocess.PIPE) - json_str = '' - - # filter out the header and footer from the json data - while True: - line = json_proc.stdout.readline().decode() - if not line: - break - if '**START JSON DATA**' in line: - continue - if '**END JSON DATA**' in line: - continue - - json_str += line - - j_obj = json.loads(json_str) + j_obj = json_from_exec(exec) - return app_from_json(j_obj, problem_names=problem_names) + return problems_from_json(j_obj, problem_names=problem_names) From 72d3bf054da2d7d086318be09f6c114b7c715fb2 Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Mon, 27 Nov 2023 13:23:55 +0000 Subject: [PATCH 02/62] add a method to organise all MOOSE block types --- catbird/cbird.py | 76 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/catbird/cbird.py b/catbird/cbird.py index a698001..b79d2a0 100644 --- a/catbird/cbird.py +++ b/catbird/cbird.py @@ -180,6 +180,82 @@ def problems_from_json(json_file, problem_names=None): return out +def parse_blocks(json_obj): + """ + Returns the a dictionary of block types corresponding to the MOOSE application described + by the json file. + + Parameters + ---------- + json_obj : dict + Dictionary of full MOOSE object tree + + Returns + ------- + dict + Dictionary of available block types organised by category + """ + + # get problems block + block_name_list = json_obj['blocks'].keys() + + # Systems have a type == None + systems=[] + + # Do not have a type, do have a sub-block + nested_systems=[] + + # Fundamental blocks are top level-blocks with a type + fundamental_blocks={} + + # Blocks which may have many entries, each with a type + nested_blocks={} + + types_key='types' + wildcard_key='star' + nested_key='subblocks' + nested_block_key='subblock_types' + for block_name in block_name_list: + block_dict_now = json_obj['blocks'][block_name] + if types_key in block_dict_now.keys(): + try : + # If dict + block_types_now = block_dict_now[types_key].keys() + fundamental_blocks[block_name]=block_types_now + except AttributeError : + # Otherwise + block_types_now = block_dict_now[types_key] + if block_types_now == None: + systems.append(block_name) + continue + + #print(block_name," available types: ", block_types_now) + elif wildcard_key in block_dict_now.keys() and nested_block_key in block_dict_now[wildcard_key].keys(): + try: + types_now = block_dict_now[wildcard_key][nested_block_key].keys() + nested_blocks[block_name]=types_now + except AttributeError : + types_now = block_dict_now[wildcard_key][nested_block_key] + if types_now == None: + nested_systems.append(block_name) + continue + + elif nested_key in block_dict_now.keys(): + nested_systems.append(block_name) + else: + print(block_name," has keys: ",block_dict_now.keys()) + raise RuntimeError("unhandled block category") + + + parsed_block_list={} + parsed_block_list["Systems"]=systems + parsed_block_list["Nested systems"]=nested_systems + parsed_block_list["Fundamental blocks"]=fundamental_blocks + parsed_block_list["Nested blocks"]=nested_blocks + + return parsed_block_list + + def parse_problems(json_obj, problem_names=None): # get problems block problems = json_obj['blocks']['Problem']['types'] From e1a8ad572d0340b822d91d5d89949da586981c4a Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Mon, 27 Nov 2023 16:00:52 +0000 Subject: [PATCH 03/62] generalise object extraction to any fundamental type (not nested) --- catbird/cbird.py | 107 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 81 insertions(+), 26 deletions(-) diff --git a/catbird/cbird.py b/catbird/cbird.py index b79d2a0..757b948 100644 --- a/catbird/cbird.py +++ b/catbird/cbird.py @@ -149,6 +149,27 @@ def json_from_exec(exec): return j_obj +def write_json(json_dict_out,name): + """ + Write a dictionary in JSON format + + Parameters + ---------- + json_dict_out : dict + name: str + Save as name.json + """ + json_output = json.dumps(json_dict_out, indent=4) + json_name=name + if json_name.find(".json") < 0 : + json_name = name+".json" + + with open(json_name, "w") as fh: + fh.write(json_output) + fh.write("\n") + print("Wrote to ",json_name) + + def problems_from_json(json_file, problem_names=None): """ Returns the Python objects corresponding to the MOOSE application described @@ -179,7 +200,6 @@ def problems_from_json(json_file, problem_names=None): return out - def parse_blocks(json_obj): """ Returns the a dictionary of block types corresponding to the MOOSE application described @@ -220,7 +240,7 @@ def parse_blocks(json_obj): if types_key in block_dict_now.keys(): try : # If dict - block_types_now = block_dict_now[types_key].keys() + block_types_now = list(block_dict_now[types_key].keys()) fundamental_blocks[block_name]=block_types_now except AttributeError : # Otherwise @@ -232,7 +252,7 @@ def parse_blocks(json_obj): #print(block_name," available types: ", block_types_now) elif wildcard_key in block_dict_now.keys() and nested_block_key in block_dict_now[wildcard_key].keys(): try: - types_now = block_dict_now[wildcard_key][nested_block_key].keys() + types_now = list(block_dict_now[wildcard_key][nested_block_key].keys()) nested_blocks[block_name]=types_now except AttributeError : types_now = block_dict_now[wildcard_key][nested_block_key] @@ -257,22 +277,52 @@ def parse_blocks(json_obj): def parse_problems(json_obj, problem_names=None): - # get problems block - problems = json_obj['blocks']['Problem']['types'] + return parse_fundamental_blocks(json_obj,'Problem',category_names=problem_names) + + +def get_fundamental_block_types(json_obj,category): + return json_obj['blocks'][category]['types'] + +def parse_fundamental_blocks(json_obj,category,category_names=None): + """ + Make python objects out of MOOSE syntax for a fundamental category of block + (E.g. Executioner, Problem) + + Parameters + ---------- + json_obj : dict + A dictionary of all MOOSE objects + + category: str + A string naming the category of fundamental MOOSE block + + category_names: list(str) + Optional field. If provided, only return objects for specified types. + + Returns + ------- + dict + A dictionary of pythonised MOOSE objects of the given category. + """ + + fundamental_blocks = get_fundamental_block_types(json_obj,category) instances_out = dict() - for problem, block in problems.items(): + for block_type, block_attributes in fundamental_blocks.items(): # skip any blocks that we aren't looking for - if problem_names is not None and problem not in problem_names: + if category_names is not None and block_type not in category_names: continue - params = block['parameters'] + # Todo add auto-documntations + #dict_keys(['description', 'file_info', 'label', 'moose_base', 'parameters', 'parent_syntax', 'register_file', 'syntax_path']) + + params = block_attributes['parameters'] - # create new subclass of Catbird with a name that matches the problem - new_cls = type(problem, (Catbird,), dict()) + # create new subclass of Catbird with a name that matches the block_type + new_cls = type(block_type, (Catbird,), dict()) - # loop over the problem parameters + # loop over the block_type parameters for param_name, param_info in params.items(): # determine the type of the parameter attr_types = tuple(type_mapping[t] for t in param_info['basic_type'].split(':')) @@ -291,32 +341,32 @@ def parse_problems(json_obj, problem_names=None): values = param_info['options'].split() allowed_values = [_convert_to_type(attr_type, v) for v in values] - # apply the default value if provided + # apply the default value if provided # TODO: default values need to be handled differently. They are replacing # properties in the type definition as they are now default = None - if 'default' in param_info and param_info['default'] != 'none': - # only supporting defaults for one dimensional dim types - vals = [_convert_to_type(attr_type, v) for v in param_info['default'].split()] - if ndim == 0: - default = vals[0] - else: - default = np.array(vals) + if 'default' in param_info.keys() and param_info['default'] != None: + default = _convert_to_type(attr_type, param_info['default']) + # # only supporting defaults for one dimensional dim types + # vals = [_convert_to_type(attr_type, v) for v in param_info['default'].split()] + # if ndim == 0: + # default = vals[0] + # else: + # default = np.array(vals) # add an attribute to the class instance for this parameter new_cls.newattr(param_name, - attr_type, - desc=param_info.get('description'), - default=default, - dim=ndim, - allowed_vals=allowed_values) + attr_type, + desc=param_info.get('description'), + default=default, + dim=ndim, + allowed_vals=allowed_values) # insert new instance into the output dictionary - instances_out[problem] = new_cls + instances_out[block_type] = new_cls return instances_out - def problem_from_exec(exec, problem_names=None): """ Returns the Python objects corresponding to the MOOSE @@ -336,3 +386,8 @@ def problem_from_exec(exec, problem_names=None): j_obj = json_from_exec(exec) return problems_from_json(j_obj, problem_names=problem_names) + +def export_all_blocks_from_exec(exec,name): + j_obj = json_from_exec(exec) + block_dict=parse_blocks(j_obj) + write_json(block_dict,name) From 90905fffa6015c54f740a3e524de44371c9fd7df Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Mon, 27 Nov 2023 17:08:28 +0000 Subject: [PATCH 04/62] generalise object extraction to any nested block type --- catbird/cbird.py | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/catbird/cbird.py b/catbird/cbird.py index 757b948..b478d8f 100644 --- a/catbird/cbird.py +++ b/catbird/cbird.py @@ -216,7 +216,7 @@ def parse_blocks(json_obj): Dictionary of available block types organised by category """ - # get problems block + # Get all top level categories of block block_name_list = json_obj['blocks'].keys() # Systems have a type == None @@ -277,13 +277,29 @@ def parse_blocks(json_obj): def parse_problems(json_obj, problem_names=None): - return parse_fundamental_blocks(json_obj,'Problem',category_names=problem_names) + return parse_blocks_types(json_obj,'Problem',category_names=problem_names) +def get_block_types(json_obj,category): + block_types=None -def get_fundamental_block_types(json_obj,category): - return json_obj['blocks'][category]['types'] + if category not in json_obj['blocks'].keys(): + msg="Unknown block name {}".format(category) + raise RuntimeError(msg) -def parse_fundamental_blocks(json_obj,category,category_names=None): + if 'types' in json_obj['blocks'][category].keys(): + block_types =json_obj['blocks'][category]['types'] + elif 'star' in json_obj['blocks'][category].keys() and 'subblock_types' in json_obj['blocks'][category]['star'].keys(): + block_types=json_obj['blocks'][category]['star']['subblock_types'] + else: + msg="Catergory {} does not have a type".format(category) + raise RuntimeError(msg) + + return block_types + + + + +def parse_blocks_types(json_obj,category,category_names=None): """ Make python objects out of MOOSE syntax for a fundamental category of block (E.g. Executioner, Problem) @@ -305,11 +321,11 @@ def parse_fundamental_blocks(json_obj,category,category_names=None): A dictionary of pythonised MOOSE objects of the given category. """ - fundamental_blocks = get_fundamental_block_types(json_obj,category) + requested_blocks = get_block_types(json_obj,category) instances_out = dict() - for block_type, block_attributes in fundamental_blocks.items(): + for block_type, block_attributes in requested_blocks.items(): # skip any blocks that we aren't looking for if category_names is not None and block_type not in category_names: continue From de35eba66dc68389b180c65b131ee3ea85711cc0 Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Mon, 27 Nov 2023 19:25:52 +0000 Subject: [PATCH 05/62] collect objects in a MOOSE model --- catbird/__init__.py | 3 ++- catbird/model.py | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 catbird/model.py diff --git a/catbird/__init__.py b/catbird/__init__.py index 8821230..fb9a7f7 100644 --- a/catbird/__init__.py +++ b/catbird/__init__.py @@ -1,3 +1,4 @@ -from .cbird import * \ No newline at end of file +from .cbird import * +from .model import * diff --git a/catbird/model.py b/catbird/model.py new file mode 100644 index 0000000..47ed835 --- /dev/null +++ b/catbird/model.py @@ -0,0 +1,23 @@ +from .cbird import parse_blocks_types + +class MooseModel(): + def __init__(self,json_obj,params=None): + if params==None: + params=self.default_params() + + self.objects={} + + for category, cat_type in params.items(): + cat_dict=parse_blocks_types(json_obj,category,category_names=[cat_type]) + cat_constructor=cat_dict[cat_type] + cat_instance=cat_constructor() + self.objects[category]=cat_instance + + @staticmethod + def default_params(): + defaults = { + "Mesh" : "GeneratedMesh", + "Executioner" : "Steady", + "Problem" : "FEProblem", + } + return defaults From 40d48559f5c55767f52df090d323a94ba5eedcdd Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Wed, 29 Nov 2023 09:57:07 +0000 Subject: [PATCH 06/62] add property to contain list of all moose params --- catbird/cbird.py | 53 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/catbird/cbird.py b/catbird/cbird.py index b478d8f..bb701b3 100644 --- a/catbird/cbird.py +++ b/catbird/cbird.py @@ -27,9 +27,9 @@ class Catbird(ABC): """ Class that can add type-checked properties to itself. """ - - def __init__(self): - self.__moose_attrs__ = [] + @property + def moose_params(self): + return self._moose_params @staticmethod def check_type(name, val, attr_type): @@ -76,7 +76,6 @@ def fset(self, val): if allowed_vals is not None: self.check_vals(name, v, allowed_vals) setattr(self, '_'+name, val) - # self.__moose_attrs__ += [name] return fset @classmethod @@ -98,20 +97,42 @@ def newattr(cls, attr_name, attr_type=str, dim=0, default=None, allowed_vals=Non if doc_str: getattr(cls, attr_name).__doc__ = doc_str - def to_node(self): - """ - Create a pyhit node for this MOOSE object - """ - import pyhit + if not hasattr(cls,"_moose_params"): + setattr(cls,"_moose_params",[]) + moose_param_list_local=getattr(cls,"_moose_params") + moose_param_list_local.append(attr_name) + setattr(cls,"_moose_params",moose_param_list_local) + + # # Todo - waspify + # def to_node(self): + # """ + # Create a pyhit node for this MOOSE object + # """ + # import pyhit + + # node = pyhit.Node(hitnode=self.__class__.__name__) + + # for attr in self.__moose_attrs__: + # val = getattr(self, attr) - node = pyhit.Node(hitnode=self.__class__.__name__) + # getattr(self, '_'+name) + # if val is not None: + # node[attr] = val - for attr in self.__moose_attrs__: - val = getattr(self, attr) - if val is not None: - node[attr] = val + # return node - return node + def print_me(self): + name=self.__class__.__name__ + print("Name: ",name) + + param_list=self.moose_params + for attr_name in param_list: + attr_val = getattr(self, attr_name) + if attr_val is not None: + attr_str="{}.{}: {}".format(name,attr_name,attr_val) + else: + attr_str="{}.{}: None".format(name,attr_name) + print(attr_str) def json_from_exec(exec): @@ -297,8 +318,6 @@ def get_block_types(json_obj,category): return block_types - - def parse_blocks_types(json_obj,category,category_names=None): """ Make python objects out of MOOSE syntax for a fundamental category of block From cca96cad7b3fa2459781f2eee9da25393288ad23 Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Wed, 29 Nov 2023 11:15:57 +0000 Subject: [PATCH 07/62] enable printing --- catbird/cbird.py | 56 +++++++++++++++++++++++++++++++++++++++++++++++- catbird/model.py | 6 ++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/catbird/cbird.py b/catbird/cbird.py index bb701b3..e527301 100644 --- a/catbird/cbird.py +++ b/catbird/cbird.py @@ -31,6 +31,24 @@ class Catbird(ABC): def moose_params(self): return self._moose_params + @property + def block_name(self): + return self._block_name + + @classmethod + def set_block_name(self,val): + self._block_name=val + + @property + def level(self): + return self._level + + @classmethod + def increment_level(cls): + if not hasattr(cls,"_level"): + setattr(cls,"_level",0) + cls._level+=1 + @staticmethod def check_type(name, val, attr_type): """Checks a value's type""" @@ -121,8 +139,41 @@ def newattr(cls, attr_name, attr_type=str, dim=0, default=None, allowed_vals=Non # return node + @property + def indent(self): + indent_str="" + indent_per_level=" " + for i_level in range(0,self.level): + indent_str+=indent_per_level + return indent_str + + #@staticmethod + def attr_to_str(self,attr_name): + attr_val = getattr(self, attr_name) + attr_str="" + if attr_val is not None: + attr_val = getattr(self, attr_name) + attr_str=self.indent+'{}={}\n'.format(attr_name,attr_val) + return attr_str + + def to_str(self): + syntax_str='[{}]\n'.format(self.block_name) + param_list=self.moose_params + + # Formatting convention, start with type + if "type" in param_list: + param_list.remove("type") + syntax_str+=self.attr_to_str("type") + + for attr_name in param_list: + syntax_str+=self.attr_to_str(attr_name) + syntax_str+='[]\n' + + return syntax_str + + def print_me(self): - name=self.__class__.__name__ + name=self.block_name print("Name: ",name) param_list=self.moose_params @@ -357,6 +408,9 @@ def parse_blocks_types(json_obj,category,category_names=None): # create new subclass of Catbird with a name that matches the block_type new_cls = type(block_type, (Catbird,), dict()) + new_cls.set_block_name(category) + new_cls.increment_level() + # loop over the block_type parameters for param_name, param_info in params.items(): # determine the type of the parameter diff --git a/catbird/model.py b/catbird/model.py index 47ed835..2e880fa 100644 --- a/catbird/model.py +++ b/catbird/model.py @@ -21,3 +21,9 @@ def default_params(): "Problem" : "FEProblem", } return defaults + + def to_str(self): + model_str="" + for name, obj in self.objects.items(): + model_str+=obj.to_str() + return model_str From 7de02d1c9aa21af652a78b64e1820b14115fccab Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Wed, 29 Nov 2023 11:57:21 +0000 Subject: [PATCH 08/62] do not use a dictionary in moose model --- catbird/model.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/catbird/model.py b/catbird/model.py index 2e880fa..a4c1406 100644 --- a/catbird/model.py +++ b/catbird/model.py @@ -5,13 +5,17 @@ def __init__(self,json_obj,params=None): if params==None: params=self.default_params() - self.objects={} + self.moose_objects=[] for category, cat_type in params.items(): cat_dict=parse_blocks_types(json_obj,category,category_names=[cat_type]) cat_constructor=cat_dict[cat_type] cat_instance=cat_constructor() - self.objects[category]=cat_instance + + # Prefer non-capitalised attributes + attr_name=category.lower() + setattr(self, attr_name, cat_instance) + self.moose_objects.append(attr_name) @staticmethod def default_params(): @@ -24,6 +28,7 @@ def default_params(): def to_str(self): model_str="" - for name, obj in self.objects.items(): + for obj_type in self.moose_objects: + obj=getattr(self,obj_type) model_str+=obj.to_str() return model_str From 67b529a7a8b026ea2319859ffec628876539a8fa Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Wed, 29 Nov 2023 17:38:46 +0000 Subject: [PATCH 09/62] Added a factory --- catbird/cbird.py | 90 +++++++++++++++++++++++---- catbird/model.py | 156 +++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 209 insertions(+), 37 deletions(-) diff --git a/catbird/cbird.py b/catbird/cbird.py index e527301..b841248 100644 --- a/catbird/cbird.py +++ b/catbird/cbird.py @@ -13,6 +13,40 @@ 'String' : str, 'Array' : list} +class SyntaxBlock(): + def __init__(self, _name, _syntax_type, _known_types): + self.name=_name + self.syntax_type=_syntax_type + self.enabled=False + self.enabled_types={} + if _known_types is not None: + for known_type_name in _known_types: + self.enabled_types[known_type_name]=False + + # Store what the default type should be + self.default_type=None + + def to_dict(self): + syntax_dict={ + "name": self.name, + "syntax_type": self.syntax_type, + "enabled": self.enabled, + "enabled_types": self.enabled_types, + } + return syntax_dict + + def enable_from_dict(self,dict_in): + self.enabled=dict_in["enabled"] + self.enabled_types=dict_in["enabled_types"] + + @property + def enabled_subblocks(self): + if self.enabled_types is not None: + enabled_type_list=[ type_name for type_name, enabled in self.enabled_types.items() if enabled ] + else: + enabled_type_list=None + return enabled_type_list + # convenience function for converting types def _convert_to_type(t, val): @@ -242,6 +276,20 @@ def write_json(json_dict_out,name): print("Wrote to ",json_name) +def read_json(json_file): + """ + Load the contents of a JSON file into a dict. + + Parameters + ---------- + json_file: str + Name of JSON file + """ + json_dict = {} + with open(json_file) as handle: + json_dict = json.load(handle) + return json_dict + def problems_from_json(json_file, problem_names=None): """ Returns the Python objects corresponding to the MOOSE application described @@ -291,6 +339,9 @@ def parse_blocks(json_obj): # Get all top level categories of block block_name_list = json_obj['blocks'].keys() + #all_syntax=[] + parsed_blocks={} + # Systems have a type == None systems=[] @@ -303,50 +354,65 @@ def parse_blocks(json_obj): # Blocks which may have many entries, each with a type nested_blocks={} + types_key='types' wildcard_key='star' nested_key='subblocks' nested_block_key='subblock_types' + for block_name in block_name_list: block_dict_now = json_obj['blocks'][block_name] if types_key in block_dict_now.keys(): try : # If dict block_types_now = list(block_dict_now[types_key].keys()) - fundamental_blocks[block_name]=block_types_now + #fundamental_blocks[block_name]=block_types_now + parsed_blocks[block_name]=SyntaxBlock(block_name,"fundamental",block_types_now) + + #all_syntax.append(SyntaxBlock(block_name,"fundamental",block_types_now)) except AttributeError : # Otherwise block_types_now = block_dict_now[types_key] if block_types_now == None: - systems.append(block_name) + #systems.append(block_name) + parsed_blocks[block_name]=SyntaxBlock(block_name,"systems",None) + #all_syntax.append(SyntaxBlock(block_name,"systems",None)) continue #print(block_name," available types: ", block_types_now) elif wildcard_key in block_dict_now.keys() and nested_block_key in block_dict_now[wildcard_key].keys(): try: types_now = list(block_dict_now[wildcard_key][nested_block_key].keys()) - nested_blocks[block_name]=types_now + #nested_blocks[block_name]=types_now + parsed_blocks[block_name]=SyntaxBlock(block_name,"nested",types_now) + #all_syntax.append(SyntaxBlock(block_name,"nested",types_now)) + except AttributeError : types_now = block_dict_now[wildcard_key][nested_block_key] if types_now == None: - nested_systems.append(block_name) + #nested_systems.append(block_name) + #all_syntax.append(SyntaxBlock(block_name,"nested_system",None)) + parsed_blocks[block_name]=SyntaxBlock(block_name,"nested_system",None) continue elif nested_key in block_dict_now.keys(): - nested_systems.append(block_name) + #nested_systems.append(block_name) + #all_syntax.append(SyntaxBlock(block_name,"nested_system",None)) + parsed_blocks[block_name]=SyntaxBlock(block_name,"nested_system",None) + else: print(block_name," has keys: ",block_dict_now.keys()) raise RuntimeError("unhandled block category") - parsed_block_list={} - parsed_block_list["Systems"]=systems - parsed_block_list["Nested systems"]=nested_systems - parsed_block_list["Fundamental blocks"]=fundamental_blocks - parsed_block_list["Nested blocks"]=nested_blocks - - return parsed_block_list + # parsed_block_list={} + # parsed_block_list["Systems"]=systems + # parsed_block_list["Nested systems"]=nested_systems + # parsed_block_list["Fundamental blocks"]=fundamental_blocks + # parsed_block_list["Nested blocks"]=nested_blocks + #return parsed_block_list + return parsed_blocks def parse_problems(json_obj, problem_names=None): return parse_blocks_types(json_obj,'Problem',category_names=problem_names) diff --git a/catbird/model.py b/catbird/model.py index a4c1406..007981b 100644 --- a/catbird/model.py +++ b/catbird/model.py @@ -1,30 +1,130 @@ -from .cbird import parse_blocks_types +from .cbird import SyntaxBlock, parse_blocks, parse_blocks_types, read_json, write_json + +class Factory(): + def __init__(self,json_obj,config_file=None): + self.available_blocks=parse_blocks(json_obj) + self.constructors={} + self.set_defaults() + if config_file is not None: + self.load_config(config_file) + self.load_enabled_objects(json_obj) + + def load_enabled_objects(self,json_obj): + for enabled_block in self.enabled_syntax: + enabled_types=self.available_blocks[enabled_block].enabled_subblocks + syntax_type=self.available_blocks[enabled_block].syntax_type + if syntax_type=="fundamental" or syntax_type=="nested": + self.constructors.update(parse_blocks_types(json_obj,enabled_block,category_names=enabled_types)) + else: + msg="Unsupported syntax type {}".format(syntax_type) + raise NotImplementedError(msg) + + def construct(obj_type): + return self.constructors[obj_type]() + + def enable_syntax(self,syntax_name,enabled_types="all"): + if syntax_name not in self.available_blocks.keys(): + msg="Cannot enable unknown syntax {}".format(syntax_name) + raise RuntimeError(msg) + + self.available_blocks[syntax_name].enabled=True + + enabled_type_list=list() + if isinstance(enabled_types,list): + enabled_type_list=enabled_types + elif enabled_types=="all": + enabled_type_list=list(self.available_blocks[syntax_name].enabled_types.keys()) + else: + msg="Invalid format for enabled_types, type = {}".format(type(enabled_types)) + raise RuntimeError(msg) + + for current_type in enabled_type_list: + self.available_blocks[syntax_name].enabled_types[current_type]=True + + + def write_config(self,filename): + config_dict={} + for block_name, block in self.available_blocks.items(): + config_dict[block_name]=block.to_dict() + write_json(config_dict,filename) + + def load_config(self,filename): + config_in=read_json(filename) + #read from file + for block_name, block_dict in config_in.items(): + self.available_blocks[block_name].enable_from_dict(block_dict) + + @property + def enabled_syntax(self): + enabled=[ block_name for block_name, block in self.available_blocks.items() if block.enabled ] + return enabled + + @property + def fundamental_syntax(self): + fundamental=[ block_name for block_name, block in self.available_blocks.items() if block.syntax_type=="fundamental"] + return fundamental + + def set_defaults(self): + self.enable_syntax("Mesh",enabled_types=["FileMesh","GeneratedMesh"]) + self.enable_syntax("Executioner",enabled_types=["Steady","Transient"]) + self.enable_syntax("Problem",enabled_types=["FEProblem"]) + self.enable_syntax("Variables",enabled_types=["MooseVariable"]) class MooseModel(): - def __init__(self,json_obj,params=None): - if params==None: - params=self.default_params() - - self.moose_objects=[] - - for category, cat_type in params.items(): - cat_dict=parse_blocks_types(json_obj,category,category_names=[cat_type]) - cat_constructor=cat_dict[cat_type] - cat_instance=cat_constructor() - - # Prefer non-capitalised attributes - attr_name=category.lower() - setattr(self, attr_name, cat_instance) - self.moose_objects.append(attr_name) - - @staticmethod - def default_params(): - defaults = { - "Mesh" : "GeneratedMesh", - "Executioner" : "Steady", - "Problem" : "FEProblem", - } - return defaults + def __init__(self,json_obj,config_file=None): + self.moose_objects={} + # Create a factory for moose objects and possibly configure from file + self.factory=Factory(json_obj,config_file) + self.set_defaults() + self.create_all_objects() + + def set_defaults(self): + self.add_to_model("Executioner", "Steady") + self.add_to_model("Problem", "FEProblem") + self.add_to_model("Mesh", "GeneratedMesh") + + def create_all_objects(self): + for category, selected in self.moose_objects: + self.add_object(selected) + + def add_object(self, object_type): + obj=self.factory.construct(object_type) + # Prefer non-capitalised attributes + attr_name=category.lower() + setattr(self, attr_name, cat_instance) + + def add_to_model(self, category, category_type): + #if category not in self.moose_objects.keys(): + # self.moose_objects[category]=list() + self.moose_objects[category]=category_type + + # TODO enforce + # Cases: if fundamental , just one + # Cases: if nested , list + + + def add_fundmental_blocks(self): + # Executioner, problem, etc .. + for fundamental_block in self.factory.fundamental_syntax : + #self.add_object(fundamental_block) + + raise NotImplementedError + + # User Call me in inherited class or model file... + def add_nested_block(self): + # Variables, kernels + # Just one type + raise NotImplementedError + + # Some short-hands for common operations + def add_variable(self): + raise NotImplementedError + + def add_bc(self): + raise NotImplementedError + + def add_ic(self): + raise NotImplementedError def to_str(self): model_str="" @@ -32,3 +132,9 @@ def to_str(self): obj=getattr(self,obj_type) model_str+=obj.to_str() return model_str + + def write(self, filename): + file_handle = open(filename,'w') + file_handle.write(self.to_str()) + file_handle.close() + print("Wrote to ",filename) From 167812127d3ab54c10a1851f41205d892151a867 Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Thu, 30 Nov 2023 17:02:30 +0000 Subject: [PATCH 10/62] now support nested types like variables --- catbird/__init__.py | 1 + catbird/cbird.py | 100 ++++++++++++++++++------------ catbird/collection.py | 40 ++++++++++++ catbird/model.py | 140 +++++++++++++++++++++++++++++++----------- 4 files changed, 204 insertions(+), 77 deletions(-) create mode 100644 catbird/collection.py diff --git a/catbird/__init__.py b/catbird/__init__.py index fb9a7f7..7ab8356 100644 --- a/catbird/__init__.py +++ b/catbird/__init__.py @@ -2,3 +2,4 @@ from .cbird import * from .model import * +from .collection import * diff --git a/catbird/cbird.py b/catbird/cbird.py index b841248..3335bd7 100644 --- a/catbird/cbird.py +++ b/catbird/cbird.py @@ -59,29 +59,43 @@ def _convert_to_type(t, val): class Catbird(ABC): """ - Class that can add type-checked properties to itself. + Class to represent MOOSE syntax that can add type-checked properties to itself. """ - @property - def moose_params(self): - return self._moose_params + def __init__(self): + self._syntax_name="" - @property - def block_name(self): - return self._block_name + def set_syntax_name(self,syntax_name): + self._syntax_name=syntax_name + + @classmethod + def set_syntax_type(cls,syntax_type): + cls._syntax_type=syntax_type @classmethod - def set_block_name(self,val): - self._block_name=val + def set_syntax_category(cls,syntax_category): + cls._syntax_category=syntax_category @property - def level(self): - return self._level + def syntax_block_name(self): + if self.is_nested: + return self._syntax_name + else: + return self._syntax_category - @classmethod - def increment_level(cls): - if not hasattr(cls,"_level"): - setattr(cls,"_level",0) - cls._level+=1 + @property + def indent_level(self): + if self.is_nested: + return 2 + else: + return 1 + + @property + def is_nested(self): + return self._syntax_type =="nested" or self._syntax_type=="nested_system" + + @property + def moose_params(self): + return self._moose_params @staticmethod def check_type(name, val, attr_type): @@ -135,6 +149,11 @@ def newattr(cls, attr_name, attr_type=str, dim=0, default=None, allowed_vals=Non """Adds a property to the class""" if not isinstance(attr_name, str): raise ValueError('Attributes must be strings') + + if attr_name.find("_syntax_") != -1: + msg="'_syntax_' is reserved attribute string. Cannot add attibute {}".format(attr_name) + raise RuntimeError(msg) + prop = property(fget=cls.prop_get(attr_name, default), fset=cls.prop_set(attr_name, attr_type, dim, allowed_vals)) setattr(cls, attr_name, prop) @@ -177,10 +196,19 @@ def newattr(cls, attr_name, attr_type=str, dim=0, default=None, allowed_vals=Non def indent(self): indent_str="" indent_per_level=" " - for i_level in range(0,self.level): + for i_level in range(0,self.indent_level): indent_str+=indent_per_level return indent_str + @property + def prepend_indent(self): + indent_str="" + indent_per_level=" " + if self.indent_level > 1: + for i_level in range(0,self.indent_level-1): + indent_str+=indent_per_level + return indent_str + #@staticmethod def attr_to_str(self,attr_name): attr_val = getattr(self, attr_name) @@ -191,7 +219,7 @@ def attr_to_str(self,attr_name): return attr_str def to_str(self): - syntax_str='[{}]\n'.format(self.block_name) + syntax_str='{}[{}]\n'.format(self.prepend_indent,self.syntax_block_name) param_list=self.moose_params # Formatting convention, start with type @@ -201,7 +229,7 @@ def to_str(self): for attr_name in param_list: syntax_str+=self.attr_to_str(attr_name) - syntax_str+='[]\n' + syntax_str+='{}[]\n'.format(self.prepend_indent) return syntax_str @@ -211,7 +239,7 @@ def print_me(self): print("Name: ",name) param_list=self.moose_params - for attr_name in param_list: + for attr_name in param_list: attr_val = getattr(self, attr_name) if attr_val is not None: attr_str="{}.{}: {}".format(name,attr_name,attr_val) @@ -342,19 +370,6 @@ def parse_blocks(json_obj): #all_syntax=[] parsed_blocks={} - # Systems have a type == None - systems=[] - - # Do not have a type, do have a sub-block - nested_systems=[] - - # Fundamental blocks are top level-blocks with a type - fundamental_blocks={} - - # Blocks which may have many entries, each with a type - nested_blocks={} - - types_key='types' wildcard_key='star' nested_key='subblocks' @@ -375,7 +390,7 @@ def parse_blocks(json_obj): block_types_now = block_dict_now[types_key] if block_types_now == None: #systems.append(block_name) - parsed_blocks[block_name]=SyntaxBlock(block_name,"systems",None) + parsed_blocks[block_name]=SyntaxBlock(block_name,"system",None) #all_syntax.append(SyntaxBlock(block_name,"systems",None)) continue @@ -419,20 +434,23 @@ def parse_problems(json_obj, problem_names=None): def get_block_types(json_obj,category): block_types=None - + syntax_type="" + if category not in json_obj['blocks'].keys(): msg="Unknown block name {}".format(category) raise RuntimeError(msg) if 'types' in json_obj['blocks'][category].keys(): block_types =json_obj['blocks'][category]['types'] + syntax_type="fundamental" elif 'star' in json_obj['blocks'][category].keys() and 'subblock_types' in json_obj['blocks'][category]['star'].keys(): block_types=json_obj['blocks'][category]['star']['subblock_types'] + syntax_type="nested" else: msg="Catergory {} does not have a type".format(category) raise RuntimeError(msg) - return block_types + return block_types, syntax_type def parse_blocks_types(json_obj,category,category_names=None): @@ -457,7 +475,7 @@ def parse_blocks_types(json_obj,category,category_names=None): A dictionary of pythonised MOOSE objects of the given category. """ - requested_blocks = get_block_types(json_obj,category) + requested_blocks,syntax_type = get_block_types(json_obj,category) instances_out = dict() @@ -474,8 +492,11 @@ def parse_blocks_types(json_obj,category,category_names=None): # create new subclass of Catbird with a name that matches the block_type new_cls = type(block_type, (Catbird,), dict()) - new_cls.set_block_name(category) - new_cls.increment_level() + # Set the + new_cls.set_syntax_type(syntax_type) + + if syntax_type != "nested": + new_cls.syntax_block_name=category # loop over the block_type parameters for param_name, param_info in params.items(): @@ -537,7 +558,6 @@ def problem_from_exec(exec, problem_names=None): dict A dictionary of problem objects """ - j_obj = json_from_exec(exec) return problems_from_json(j_obj, problem_names=problem_names) diff --git a/catbird/collection.py b/catbird/collection.py new file mode 100644 index 0000000..aa2a563 --- /dev/null +++ b/catbird/collection.py @@ -0,0 +1,40 @@ +from collections.abc import MutableSet +from .cbird import Catbird + +class MOOSECollection(MutableSet): + """A collection of MOOSE (Catbird) objects""" + def __init__(self): + self.objects={} + + # Define mandatory methods + def __contains__(self,key): + return key in self.objects.keys() + + def __iter__(self): + return iter(self.objects) + + def __len__(self): + return len(self.objects) + + def add(self,obj): + # Ensure type inherits from Catbird + assert issubclass(type(obj),Catbird) + + block_name=obj.syntax_block_name + + if block_name in self.objects.keys(): + msg="Collection already contains named block {}".format(block_name) + raise RuntimeError(msg) + + # Index + self.objects[block_name]=obj + + def discard(self,key): + self.objects.pop(key) + + def to_str(self): + collection_str="[{}]\n".format(self.__class__.__name__) + for name, obj in self.objects.items(): + collection_str+=obj.to_str() + collection_str+="[]\n" + return collection_str diff --git a/catbird/model.py b/catbird/model.py index 007981b..db7dd97 100644 --- a/catbird/model.py +++ b/catbird/model.py @@ -1,4 +1,5 @@ from .cbird import SyntaxBlock, parse_blocks, parse_blocks_types, read_json, write_json +from .collection import MOOSECollection class Factory(): def __init__(self,json_obj,config_file=None): @@ -19,16 +20,33 @@ def load_enabled_objects(self,json_obj): msg="Unsupported syntax type {}".format(syntax_type) raise NotImplementedError(msg) - def construct(obj_type): - return self.constructors[obj_type]() + def construct(self,obj_type,**kwargs): + obj=self.constructors[obj_type]() - def enable_syntax(self,syntax_name,enabled_types="all"): + # Handle keyword arguments + for key, value in kwargs.items(): + if not hasattr(obj,key): + msg="Object type {} does not have attribute {}".format(obj_type,key) + raise RuntimeError() + setattr(obj, key, value) + + return obj + + def enable_syntax(self,syntax_name,default=None,enabled_types="all"): + """ + Configure what MOOSE syntax to enable. + + Objects with enabled syntax will be converted to Python classes. + """ + # Check syntax is known if syntax_name not in self.available_blocks.keys(): msg="Cannot enable unknown syntax {}".format(syntax_name) raise RuntimeError(msg) + # Enable top level block syntax self.available_blocks[syntax_name].enabled=True + # Enable sub-block types enabled_type_list=list() if isinstance(enabled_types,list): enabled_type_list=enabled_types @@ -38,9 +56,26 @@ def enable_syntax(self,syntax_name,enabled_types="all"): msg="Invalid format for enabled_types, type = {}".format(type(enabled_types)) raise RuntimeError(msg) + # Ensure default is enabled + if default not in enabled_type_list and default is not None: + enabled_type_list.append(default) + for current_type in enabled_type_list: + # Ensure valid + if current_type not in self.available_blocks[syntax_name].enabled_types.keys(): + msg="Unknown type {} for block {}".format(current_type,syntax_name) + raise KeyError(msg) + self.available_blocks[syntax_name].enabled_types[current_type]=True + # Set default for typed blocks + syntax_type = self.available_blocks[syntax_name].syntax_type + if default is not None: + self.available_blocks[syntax_name].default_type=default + elif syntax_type == "fundamental" or syntax_type == "nested": + msg="Please set a default for typed blocks" + raise RuntimeError(msg) + def write_config(self,filename): config_dict={} @@ -65,60 +100,91 @@ def fundamental_syntax(self): return fundamental def set_defaults(self): - self.enable_syntax("Mesh",enabled_types=["FileMesh","GeneratedMesh"]) - self.enable_syntax("Executioner",enabled_types=["Steady","Transient"]) - self.enable_syntax("Problem",enabled_types=["FEProblem"]) - self.enable_syntax("Variables",enabled_types=["MooseVariable"]) + self.enable_syntax("Mesh",default="FileMesh",enabled_types=["FileMesh","GeneratedMesh"]) + self.enable_syntax("Executioner",default="Steady",enabled_types=["Steady","Transient"]) + self.enable_syntax("Problem",default="FEProblem",enabled_types=["FEProblem"]) + self.enable_syntax("Variables",default="MooseVariable",enabled_types=["MooseVariable"]) class MooseModel(): def __init__(self,json_obj,config_file=None): self.moose_objects={} # Create a factory for moose objects and possibly configure from file self.factory=Factory(json_obj,config_file) + # Add attributes to this model with default assignments self.set_defaults() - self.create_all_objects() + # Envisage this being overridden downstream. def set_defaults(self): - self.add_to_model("Executioner", "Steady") - self.add_to_model("Problem", "FEProblem") - self.add_to_model("Mesh", "GeneratedMesh") + # Fundamental Types + self.add_category("Executioner", "Steady") + self.add_category("Problem", "FEProblem") + self.add_category("Mesh", "GeneratedMesh") + + def add_category(self, category, category_type, syntax_name=""): + # Ensure this is valid syntax + if category not in self.factory.enabled_syntax: + msg="Invalid block type {}".format(category) + raise RuntimeError(msg) - def create_all_objects(self): - for category, selected in self.moose_objects: - self.add_object(selected) + # First look up the syntax type + syntax_type=self.factory.available_blocks[category].syntax_type + + # How to add depends on syntax type + if syntax_type == "fundamental": + # If fundmantal, just add. We're done. + self.add_object(category,category_type) + elif syntax_type == "nested": + if not hasattr(self,category): + self.add_collection(category) + self.add_to_collection(category,category_type,syntax_name) + elif syntax_type == "system": + raise NotImplementedError() + elif syntax_type == "nested_system": + raise NotImplementedError() + else: + msg="Unhandled syntax type {}".format(syntax_type) + raise RuntimeError(msg) - def add_object(self, object_type): - obj=self.factory.construct(object_type) + # Object has been constructed, now just book-keeping + category_key=category.lower() + if category_key not in self.moose_objects.keys(): + self.moose_objects[category_key]=list() + self.moose_objects[category_key].append(category_type) + + def add_object(self,object_category,object_type,**kwargs): + obj=self.factory.construct(object_type,**kwargs) # Prefer non-capitalised attributes - attr_name=category.lower() - setattr(self, attr_name, cat_instance) + attr_name=object_category.lower() + setattr(self,attr_name,obj) - def add_to_model(self, category, category_type): - #if category not in self.moose_objects.keys(): - # self.moose_objects[category]=list() - self.moose_objects[category]=category_type + def add_collection(self, collection_type): + # E.g. Variables, Kernels, BCs, Materials + # Create new subclass of with a name that matches the collection_type + new_cls = type(collection_type, (MOOSECollection,), dict()) - # TODO enforce - # Cases: if fundamental , just one - # Cases: if nested , list + # Prefer non-capitalised attributes + attr_name=collection_type.lower() + # Construct and add the to model + setattr(self, attr_name, new_cls()) - def add_fundmental_blocks(self): - # Executioner, problem, etc .. - for fundamental_block in self.factory.fundamental_syntax : - #self.add_object(fundamental_block) + def add_to_collection(self, collection_type, object_type, syntax_name, **kwargs): + # Construct object + obj=self.factory.construct(object_type,**kwargs) + if syntax_name=="": + raise RuntimeError("Must supply syntax_name for nested syntax") - raise NotImplementedError + obj.set_syntax_name(syntax_name) - # User Call me in inherited class or model file... - def add_nested_block(self): - # Variables, kernels - # Just one type - raise NotImplementedError + # Obtain the object for this collection type + collection = getattr(self, collection_type.lower()) + + # Store in collection + collection.add(obj) # Some short-hands for common operations - def add_variable(self): - raise NotImplementedError + def add_variable(self,name,variable_type="MooseVariable"): + model.add_category("Variables",name,variable_type) def add_bc(self): raise NotImplementedError From eb660f2b0842976634eedbc5a0492a9b5adfbd41 Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Fri, 1 Dec 2023 16:24:03 +0000 Subject: [PATCH 11/62] Store extra metadata in a MooseParam so we can check default vals, provide doc --- catbird/cbird.py | 175 +++++++++++++++++++++++++++++++++++------------ catbird/model.py | 2 +- 2 files changed, 131 insertions(+), 46 deletions(-) diff --git a/catbird/cbird.py b/catbird/cbird.py index 3335bd7..6b2383e 100644 --- a/catbird/cbird.py +++ b/catbird/cbird.py @@ -56,24 +56,35 @@ def _convert_to_type(t, val): val = t(val) return val +class MooseParam(): + """ + Class to contain all information about a MOOSE parameter + """ + def __init__(self): + self.val=None + self.attr_type=None + self.default=None + self.allowed_vals=None + self.dim=0 + self.doc="" class Catbird(ABC): """ Class to represent MOOSE syntax that can add type-checked properties to itself. """ - def __init__(self): + def __init__(self): self._syntax_name="" def set_syntax_name(self,syntax_name): self._syntax_name=syntax_name - + @classmethod def set_syntax_type(cls,syntax_type): cls._syntax_type=syntax_type @classmethod def set_syntax_category(cls,syntax_category): - cls._syntax_category=syntax_category + cls._syntax_category=syntax_category @property def syntax_block_name(self): @@ -91,8 +102,8 @@ def indent_level(self): @property def is_nested(self): - return self._syntax_type =="nested" or self._syntax_type=="nested_system" - + return self._syntax_type =="nested" or self._syntax_type=="nested_system" + @property def moose_params(self): return self._moose_params @@ -114,38 +125,88 @@ def check_vals(name, val, allowed_vals): raise ValueError(f'Value {val} for attribute {name} is not one of {allowed_vals}') @staticmethod - def prop_get(name, default=None): - """Returns function for getting an attribute""" + def moose_property(name, param): + """ + Returns a property, and creates an associated class attribute whose value is that of the supplied MooseParam. + + The property setter method will change the value of the underlying MooseParam.value, checking its type is consistent + The property getter method will retrieve the value of the underlying MooseParam.value + """ + def fget(self): # set to the default value if the internal attribute doesn't exist if not hasattr(self, '_'+name): - setattr(self, '_'+name, default) - value = getattr(self, '_'+name) - return value - return fget + setattr(self, '_'+name, param) + param_now = getattr(self, '_'+name) + return param_now.val - @staticmethod - def prop_set(name, attr_type, dim=0, allowed_vals=None): - """Returns a function for setting an attribute""" def fset(self, val): - if dim == 0: - self.check_type(name, val, attr_type) - if allowed_vals is not None: - self.check_vals(name, val, allowed_vals) - setattr(self, '_'+name, val) + if param.dim == 0: + self.check_type(name, val, param.attr_type) + if param.allowed_vals is not None: + self.check_vals(name, val, param.allowed_vals) else: val = np.asarray(val) - self.check_type(name, val.flat[0].item(), attr_type) - if len(val.shape) != dim: + self.check_type(name, val.flat[0].item(), param.attr_type) + if len(val.shape) != param.dim: raise ValueError(f'Dimensionality is incorrect. Expects a {dim}-D array.') for v in val.flatten(): - if allowed_vals is not None: + if param.allowed_vals is not None: self.check_vals(name, v, allowed_vals) - setattr(self, '_'+name, val) - return fset + + param_now = getattr(self, '_'+name) + param_now.val=val + setattr(self, '_'+name, param_now) + + return property(fget,fset) + + # @staticmethod + # def prop_get(name, default=None): + # """Returns function for getting an attribute""" + # def fget(self): + # # set to the default value if the internal attribute doesn't exist + # if not hasattr(self, '_'+name): + # setattr(self, '_'+name, default) + # value = getattr(self, '_'+name) + # return value + # return fget + + # @staticmethod + # def prop_set(name, attr_type, dim=0, allowed_vals=None,doc=""): + # """Returns a function for setting an attribute""" + # def fset(self, val): + # if dim == 0: + # self.check_type(name, val, attr_type) + # if allowed_vals is not None: + # self.check_vals(name, val, allowed_vals) + # setattr(self, '_'+name, val) + # #setattr(self, '_'+name, val) + # else: + # val = np.asarray(val) + # self.check_type(name, val.flat[0].item(), attr_type) + # if len(val.shape) != dim: + # raise ValueError(f'Dimensionality is incorrect. Expects a {dim}-D array.') + # for v in val.flatten(): + # if allowed_vals is not None: + # self.check_vals(name, v, allowed_vals) + # setattr(self, '_'+name, val) + # return fset + + # @staticmethod + # def prop_doc(name, doc_str=""): + # def fdoc(self): + # print("retrieve doc") + # doc_str_now="" + # if doc_str != "": + # doc_str_now=doc_str + # else: + # value = getattr(self, '_'+name) + # doc_str_now=value.__doc__ + # return doc_str_now + # return fdoc @classmethod - def newattr(cls, attr_name, attr_type=str, dim=0, default=None, allowed_vals=None, desc=None): + def newattr(cls, attr_name, attr_type=str, dim=0, default=None, allowed_vals=None, description=None): """Adds a property to the class""" if not isinstance(attr_name, str): raise ValueError('Attributes must be strings') @@ -153,21 +214,36 @@ def newattr(cls, attr_name, attr_type=str, dim=0, default=None, allowed_vals=Non if attr_name.find("_syntax_") != -1: msg="'_syntax_' is reserved attribute string. Cannot add attibute {}".format(attr_name) raise RuntimeError(msg) - - prop = property(fget=cls.prop_get(attr_name, default), - fset=cls.prop_set(attr_name, attr_type, dim, allowed_vals)) - setattr(cls, attr_name, prop) - # set attribute docstring + # Set attribute docstring doc_str = f'\nType: {attr_type.__name__}\n' - if desc is not None: - doc_str += desc + if description is not None: + doc_str += description if allowed_vals is not None: doc_str += f'\nValues: {allowed_vals}' - if doc_str: - getattr(cls, attr_name).__doc__ = doc_str - + # Store parameter details in a structure + moose_param=MooseParam() + if default is not None: + moose_param.val=default + else: + moose_param.val=attr_type() + moose_param.attr_type=attr_type + moose_param.default=default + moose_param.dim=dim + moose_param.allowed_vals=allowed_vals + moose_param.doc=doc_str + + # Define a property and add to class (args are functions) + # Should be able to add a docstring here.... + #prop = property(fget=cls.prop_get(attr_name, default), + # fset=cls.prop_set(attr_name, attr_type, dim, allowed_vals, doc_str)) + #setattr(cls, attr_name, prop) + + # Add attribute to the class using a method which returns a property + setattr(cls, attr_name, cls.moose_property(attr_name,moose_param)) + + # Keep track of the attributes we've added if not hasattr(cls,"_moose_params"): setattr(cls,"_moose_params",[]) moose_param_list_local=getattr(cls,"_moose_params") @@ -209,37 +285,46 @@ def prepend_indent(self): indent_str+=indent_per_level return indent_str - #@staticmethod - def attr_to_str(self,attr_name): + def is_default(self,attr_name): attr_val = getattr(self, attr_name) + param = getattr(self, "_"+attr_name) + default_val = param.default + if default_val is None: + default_val = param.attr_type() + return attr_val == default_val + + def attr_to_str(self,attr_name,print_default=False): attr_str="" + if self.is_default(attr_name) and not print_default: + return attr_str + + attr_val = getattr(self, attr_name) if attr_val is not None: attr_val = getattr(self, attr_name) attr_str=self.indent+'{}={}\n'.format(attr_name,attr_val) return attr_str - def to_str(self): + def to_str(self,print_default=False): syntax_str='{}[{}]\n'.format(self.prepend_indent,self.syntax_block_name) param_list=self.moose_params # Formatting convention, start with type if "type" in param_list: param_list.remove("type") - syntax_str+=self.attr_to_str("type") + syntax_str+=self.attr_to_str("type",True) for attr_name in param_list: - syntax_str+=self.attr_to_str(attr_name) + syntax_str+=self.attr_to_str(attr_name,print_default) syntax_str+='{}[]\n'.format(self.prepend_indent) return syntax_str - def print_me(self): name=self.block_name print("Name: ",name) param_list=self.moose_params - for attr_name in param_list: + for attr_name in param_list: attr_val = getattr(self, attr_name) if attr_val is not None: attr_str="{}.{}: {}".format(name,attr_name,attr_val) @@ -435,7 +520,7 @@ def parse_problems(json_obj, problem_names=None): def get_block_types(json_obj,category): block_types=None syntax_type="" - + if category not in json_obj['blocks'].keys(): msg="Unknown block name {}".format(category) raise RuntimeError(msg) @@ -494,7 +579,7 @@ def parse_blocks_types(json_obj,category,category_names=None): # Set the new_cls.set_syntax_type(syntax_type) - + if syntax_type != "nested": new_cls.syntax_block_name=category @@ -533,7 +618,7 @@ def parse_blocks_types(json_obj,category,category_names=None): # add an attribute to the class instance for this parameter new_cls.newattr(param_name, attr_type, - desc=param_info.get('description'), + description=param_info.get('description'), default=default, dim=ndim, allowed_vals=allowed_values) diff --git a/catbird/model.py b/catbird/model.py index db7dd97..2a49be0 100644 --- a/catbird/model.py +++ b/catbird/model.py @@ -184,7 +184,7 @@ def add_to_collection(self, collection_type, object_type, syntax_name, **kwargs) # Some short-hands for common operations def add_variable(self,name,variable_type="MooseVariable"): - model.add_category("Variables",name,variable_type) + self.add_category("Variables",variable_type,name) def add_bc(self): raise NotImplementedError From 6efa8d0efc07a281414e923d8042031610374e03 Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Fri, 1 Dec 2023 16:43:01 +0000 Subject: [PATCH 12/62] get documentation working --- catbird/cbird.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/catbird/cbird.py b/catbird/cbird.py index 6b2383e..5c76059 100644 --- a/catbird/cbird.py +++ b/catbird/cbird.py @@ -158,7 +158,12 @@ def fset(self, val): param_now.val=val setattr(self, '_'+name, param_now) - return property(fget,fset) + def fdel(self): + param_now = getattr(self, '_'+name) + del param_now + + + return property(fget,fset,fdel,param.doc) # @staticmethod # def prop_get(name, default=None): From d0e81a087469cbd957f9d0540c933f5043ba7fce Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Mon, 4 Dec 2023 15:52:38 +0000 Subject: [PATCH 13/62] WIP attempt to add non-typed system syntax --- catbird/cbird.py | 70 +++++++++++++++++++++++++++++++++++++++--------- catbird/model.py | 19 +++++++++++-- 2 files changed, 74 insertions(+), 15 deletions(-) diff --git a/catbird/cbird.py b/catbird/cbird.py index 5c76059..ff80821 100644 --- a/catbird/cbird.py +++ b/catbird/cbird.py @@ -522,25 +522,69 @@ def parse_blocks(json_obj): def parse_problems(json_obj, problem_names=None): return parse_blocks_types(json_obj,'Problem',category_names=problem_names) -def get_block_types(json_obj,category): +def get_block_types(json_obj,block_name): block_types=None syntax_type="" - if category not in json_obj['blocks'].keys(): - msg="Unknown block name {}".format(category) - raise RuntimeError(msg) + blocks_dict=json_obj['blocks'] - if 'types' in json_obj['blocks'][category].keys(): - block_types =json_obj['blocks'][category]['types'] - syntax_type="fundamental" - elif 'star' in json_obj['blocks'][category].keys() and 'subblock_types' in json_obj['blocks'][category]['star'].keys(): - block_types=json_obj['blocks'][category]['star']['subblock_types'] - syntax_type="nested" - else: - msg="Catergory {} does not have a type".format(category) + if block_name not in blocks_dict.keys(): + msg="Unknown block name {}".format(block_name) raise RuntimeError(msg) - return block_types, syntax_type + current_block_dict=blocks_dict[block_name] + + syntax_type_to_block_types={ + "fundamental":{}, + "system":{}, + "nested":{}, + "nested_system":{}, + } + + # 4 cases, but not limited to single type at once + if 'types' in current_block_dict.keys() and current_block_dict['types'] is not None: + block_types=current_block_dict['types'] + syntax_type_to_block_types["fundamental"].update(block_types) + + if 'star' in current_block_dict.keys() and current_block_dict['star'] is not None: + if 'subblock_types' in current_block_dict['star'].keys(): + block_types=current_block_dict['star']['subblock_types'] + if block_types is not None: + syntax_type_to_block_types["nested"].update(block_types) + + if 'subblocks' in current_block_dict.keys() and current_block_dict['subblocks'] is not None: + for subblock_name in current_block_dict['subblocks'].keys(): + subblock_dict=current_block_dict['subblocks'][subblock_name] + + if 'types' in subblock_dict.keys() and subblock_dict['types'] is not None: + block_types=subblock_dict['types'] + syntax_type_to_block_types["system"].update(block_types) + + if 'star' in subblock_dict.keys() and subblock_dict['star'] is not None: + if 'subblock_types' in subblock_dict['star'].keys(): + block_types=subblock_dict['star']['subblock_types'] + if block_types is not None: + syntax_type_to_block_types["nested_system"].update(block_types) + + count_types=0 + for syntax_type in syntax_type_to_block_types.keys(): + block_types=syntax_type_to_block_types[syntax_type] + if len(block_types) > 0: + count_types+=1 + + if count_types == 0: + msg="Block {} is undocumented".format(block_name) + print(msg) + #raise RuntimeError(msg) + #block_types=None + #syntax_type="Unknown" + + elif count_types > 1: + msg="Block {} is has {} types".format(block_name,len(syntax_type_to_block_types)) + print(msg) + + #return block_types, syntax_type + return syntax_type_to_block_types def parse_blocks_types(json_obj,category,category_names=None): diff --git a/catbird/model.py b/catbird/model.py index 2a49be0..4922e4c 100644 --- a/catbird/model.py +++ b/catbird/model.py @@ -96,8 +96,23 @@ def enabled_syntax(self): @property def fundamental_syntax(self): - fundamental=[ block_name for block_name, block in self.available_blocks.items() if block.syntax_type=="fundamental"] - return fundamental + return self.get_syntax_list("fundamental") + + @property + def nested_syntax(self): + return self.get_syntax_list("nested") + + @property + def systems_syntax(self): + return self.get_syntax_list("system") + + @property + def nested_systems_syntax(self): + return self.get_syntax_list("nested_system") + + def get_syntax_list(self,syntax_type_str): + syntax_list=[ block_name for block_name, block in self.available_blocks.items() if block.syntax_type==syntax_type_str] + return syntax_list def set_defaults(self): self.enable_syntax("Mesh",default="FileMesh",enabled_types=["FileMesh","GeneratedMesh"]) From 83a19d038716df30ef5da1a9a4a6b1740c1d4e30 Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Thu, 7 Dec 2023 14:00:35 +0000 Subject: [PATCH 14/62] add optional arugment print_default in to_str methods --- catbird/collection.py | 4 ++-- catbird/model.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/catbird/collection.py b/catbird/collection.py index aa2a563..d1dc1d4 100644 --- a/catbird/collection.py +++ b/catbird/collection.py @@ -32,9 +32,9 @@ def add(self,obj): def discard(self,key): self.objects.pop(key) - def to_str(self): + def to_str(self,print_default=False): collection_str="[{}]\n".format(self.__class__.__name__) for name, obj in self.objects.items(): - collection_str+=obj.to_str() + collection_str+=obj.to_str(print_default) collection_str+="[]\n" return collection_str diff --git a/catbird/model.py b/catbird/model.py index 4922e4c..b4c58b4 100644 --- a/catbird/model.py +++ b/catbird/model.py @@ -207,11 +207,11 @@ def add_bc(self): def add_ic(self): raise NotImplementedError - def to_str(self): + def to_str(self,print_default=False): model_str="" for obj_type in self.moose_objects: obj=getattr(self,obj_type) - model_str+=obj.to_str() + model_str+=obj.to_str(print_default) return model_str def write(self, filename): From b7c5d1777829809584b85f4e57a8a6a9757e7037 Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Thu, 7 Dec 2023 14:02:56 +0000 Subject: [PATCH 15/62] update to gitignore --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index bee8a64..8691be0 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ __pycache__ +*~ +*# +build +catbird.egg-info From c0587108e3ee734ae873dff524009308a17e18ff Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Fri, 8 Dec 2023 09:34:56 +0000 Subject: [PATCH 16/62] add more syntax types --- catbird/cbird.py | 50 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/catbird/cbird.py b/catbird/cbird.py index ff80821..b5ad00c 100644 --- a/catbird/cbird.py +++ b/catbird/cbird.py @@ -539,9 +539,13 @@ def get_block_types(json_obj,block_name): "system":{}, "nested":{}, "nested_system":{}, + "action":{}, + "double_nested":{}, } - # 4 cases, but not limited to single type at once + # 6 cases, but not limited to single type at once + # TODO this is awful... refactor + # Suggest recursing down until found a "parameter" key if 'types' in current_block_dict.keys() and current_block_dict['types'] is not None: block_types=current_block_dict['types'] syntax_type_to_block_types["fundamental"].update(block_types) @@ -552,19 +556,57 @@ def get_block_types(json_obj,block_name): if block_types is not None: syntax_type_to_block_types["nested"].update(block_types) + if 'actions' in current_block_dict['star'].keys(): + block_types=current_block_dict['star']['actions'] + if block_types is not None: + syntax_type_to_block_types["nested_action"].update(block_types) + if 'subblocks' in current_block_dict.keys() and current_block_dict['subblocks'] is not None: + + system_type_dict={} + nested_type_dict={} + double_nested_type_dict={} + for subblock_name in current_block_dict['subblocks'].keys(): subblock_dict=current_block_dict['subblocks'][subblock_name] if 'types' in subblock_dict.keys() and subblock_dict['types'] is not None: block_types=subblock_dict['types'] - syntax_type_to_block_types["system"].update(block_types) + system_type_dict[subblock_name]=block_types if 'star' in subblock_dict.keys() and subblock_dict['star'] is not None: if 'subblock_types' in subblock_dict['star'].keys(): block_types=subblock_dict['star']['subblock_types'] if block_types is not None: - syntax_type_to_block_types["nested_system"].update(block_types) + nested_type_dict[subblock_name]=block_types + + if 'subblocks' in subblock_dict.keys() and subblock_dict['subblocks'] is not None: + + double_nested_type_dict[subblock_name]={} + + for subsubblock_name in subblock_dict['subblocks'].keys(): + subsubblock_dict=subblock_dict['subblocks'][subsubblock_name] + + if 'actions' in subsubblock_dict.keys() and subsubblock_dict['actions'] is not None: + double_nested_type_dict[subblock_name][subsubblock_name]=subsubblock_dict['actions'] + + if 'star' in subsubblock_dict.keys() and subsubblock_dict['star'] is not None: + if 'actions' in subsubblock_dict['star'].keys() and subsubblock_dict['star']['actions'] is not None: + double_nested_type_dict[subblock_name][subsubblock_name]=subsubblock_dict['star']['actions'] + + + if len(system_type_dict) >0: + syntax_type_to_block_types["system"].update(system_type_dict) + if len(nested_type_dict) >0: + syntax_type_to_block_types["nested_system"].update(nested_type_dict) + if len(double_nested_type_dict) >0: + syntax_type_to_block_types["double_nested"].update(double_nested_type_dict) + + + if 'actions' in current_block_dict.keys() and current_block_dict['actions'] is not None: + block_types=current_block_dict['actions'] + syntax_type_to_block_types["action"].update(block_types) + count_types=0 for syntax_type in syntax_type_to_block_types.keys(): @@ -580,7 +622,7 @@ def get_block_types(json_obj,block_name): #syntax_type="Unknown" elif count_types > 1: - msg="Block {} is has {} types".format(block_name,len(syntax_type_to_block_types)) + msg="Block {} is has {} types".format(block_name,count_types) print(msg) #return block_types, syntax_type From 2c91218f57a31f4c1311dce25984b57fe10a6c7a Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Fri, 8 Dec 2023 14:23:40 +0000 Subject: [PATCH 17/62] Too many edge cases - find parameters through recursion instead --- catbird/cbird.py | 163 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 162 insertions(+), 1 deletion(-) diff --git a/catbird/cbird.py b/catbird/cbird.py index b5ad00c..71febc9 100644 --- a/catbird/cbird.py +++ b/catbird/cbird.py @@ -4,7 +4,7 @@ import numpy as np from pathlib import Path import subprocess - +from copy import deepcopy type_mapping = {'Integer' : int, 'Boolean' : bool, @@ -13,6 +13,120 @@ 'String' : str, 'Array' : list} +_relation_syntax=["blocks","subblocks","actions","star","types","subblock_types"] + +class SyntaxRegistry(): + def __init__(self, syntax_paths_in): + self.syntax_dict={} + + assert isinstance(syntax_paths_in,list) + for path_now in syntax_paths_in: + self._recurse_path(path_now) + + + def _recurse_path(self, path_in, children=None): + syntax=SyntaxPath(path_in) + self._add_syntax(syntax) + + # Add / update parents + if syntax.parent_key is not None: + if syntax.parent_key not in self.syntax_dict: + self._recurse_path(syntax.parent_path) + # Add current node to parent + self.syntax_dict[syntax.parent_key].add_child(syntax) + + def _add_syntax(self, syntax): + assert isinstance(syntax,SyntaxPath) + assert syntax.unique_key not in self.syntax_dict.keys() + self.syntax_dict[syntax.unique_key]=syntax + + + @property + def root_keys(self): + return [ unique_key for unique_key, syntax in self.syntax_dict.items() if syntax.is_root ] + +class SyntaxPath(): + def __init__(self, syntax_path_in): + # Initial values + self.name="" + self.unique_key="" + self.has_params=False + self.is_root=False + self.parent_path=None + self.parent_relation_path=None + self.child_paths=[] + self.path=deepcopy(syntax_path_in) + + + syntax_path=deepcopy(syntax_path_in) + + # Type assertions + assert isinstance(syntax_path,list) + assert len(syntax_path)>1 + for key in syntax_path: + assert isinstance(key,str) + + # Check for parameters + pos_now=len(syntax_path)-1 + key_now = syntax_path.pop(pos_now) + if key_now == "parameters": + self.has_params=True + self.path.pop(pos_now) + pos_now=pos_now-1 + key_now = syntax_path.pop(pos_now) + + # Set object name + self.name=key_now + self.unique_key=self._get_lookup_key(syntax_path,key_now) + + if len(syntax_path) > 1 : + relation_path=[] + found_parent=False + while not found_parent and len(syntax_path) > 0: + pos_now=len(syntax_path)-1 + test_key=syntax_path.pop(pos_now) + if test_key in _relation_syntax: + relation_path.insert(0,test_key) + else: + found_parent=True + syntax_path.append(test_key) + + if not found_parent: + raise RuntimeError("Should not get here") + self.parent_relation=relation_path + self.parent_path=syntax_path + + else: + self.is_root=True + + + def _key_from_list(self,path_in): + path_str="" + for key in path_in: + path_str+=key + path_str+="/" + return path_str + + def _get_lookup_key(self,path_in,name_in): + lookup_path=self._key_from_list(path_in) + lookup_path+=name_in + return lookup_path + + @property + def parent_key(self): + if not self.is_root: + parent_path_now=deepcopy(self.parent_path) + parent_len=len(parent_path_now) + parent_name=parent_path_now.pop(parent_len-1) + return self._get_lookup_key(parent_path_now,parent_name) + else: + return None + + def add_child(self, child_syntax): + assert isinstance(child_syntax,SyntaxPath) + self.child_paths.append(child_syntax.unique_key) + + class SyntaxBlock(): def __init__(self, _name, _syntax_type, _known_types): self.name=_name @@ -56,6 +170,21 @@ def _convert_to_type(t, val): val = t(val) return val +def get_params(json_dict,syntax): + assert isinstance(syntax,SyntaxPath) + assert syntax.has_params + key_list=deepcopy(syntax.path) + dict_now=json_dict + while len(key_list) > 0: + key_now=key_list.pop(0) + obj_now=dict_now[key_now] + + assert isinstance(obj_now,dict) + dict_now=deepcopy(obj_now) + + params=dict_now["parameters"] + return params + class MooseParam(): """ Class to contain all information about a MOOSE parameter @@ -438,6 +567,38 @@ def problems_from_json(json_file, problem_names=None): return out +def key_search_recurse(dict_in, test_path, key_test, level_stop=15): + """ + Parse blocks recursively until we hit the given key + """ + if not isinstance(dict_in,dict): + return list() + + if len(test_path) == level_stop: + return list() + + if key_test in dict_in.keys(): + # Success at leaf node! Found key, return path to here + success_path=deepcopy(test_path) + success_path.append(key_test) + return [ success_path ] + + success_paths=[] + for key_now, test_obj in dict_in.items(): + # Path to be tested + path_now=deepcopy(test_path) + path_now.append(key_now) + + paths_to_success=key_search_recurse(test_obj,path_now,key_test,level_stop) + + # If search fails, paths will be empty + # Otherwise add to our known list of success paths from this node + if len( paths_to_success ) != 0: + success_paths.extend(paths_to_success) + + return success_paths + + def parse_blocks(json_obj): """ Returns the a dictionary of block types corresponding to the MOOSE application described From 15765e1aa6d20c94742aa0d522b852a36b7e5f71 Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Fri, 8 Dec 2023 18:14:57 +0000 Subject: [PATCH 18/62] create syntax blocks via a registry --- catbird/cbird.py | 190 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 153 insertions(+), 37 deletions(-) diff --git a/catbird/cbird.py b/catbird/cbird.py index 71febc9..9ffabbf 100644 --- a/catbird/cbird.py +++ b/catbird/cbird.py @@ -14,6 +14,14 @@ 'Array' : list} _relation_syntax=["blocks","subblocks","actions","star","types","subblock_types"] +_relation_shorthands={ + "types": "types/", + "actions":"actions/", + "systems": "subblocks/", + "type collections" : "star/subblock_types/", + "action collections": "star/actions/", + "system collections": "star/subblocks/" +} class SyntaxRegistry(): def __init__(self, syntax_paths_in): @@ -40,10 +48,56 @@ def _add_syntax(self, syntax): assert syntax.unique_key not in self.syntax_dict.keys() self.syntax_dict[syntax.unique_key]=syntax - - @property - def root_keys(self): - return [ unique_key for unique_key, syntax in self.syntax_dict.items() if syntax.is_root ] + def get_children_of_type(self,syntax_key,relation_type): + children=[] + parent=self.syntax_dict[syntax_key] + if parent.has_child_type(relation_type): + for child_path in parent.child_paths[relation_type]: + child=self.syntax_dict[child_path] + children.append(child.name) + return children + + def get_available_syntax(self,syntax_key): + available={} + for shortname,relation in _relation_shorthands.items(): + syntax_list=self.get_children_of_type(syntax_key,relation) + if len(syntax_list)>0: + available[shortname]=syntax_list + if len(available.keys()) == 0: + available=None + return available + + def make_block(self,syntax_key): + syntax=self.syntax_dict[syntax_key] + + block=SyntaxBlock() + block.name=syntax.name + block.has_params=syntax.has_params + block.available_syntax=self.get_available_syntax(syntax_key) + + if not syntax.is_root: + # Recurse until we find root + syntax_now=syntax + depth=0 + while not syntax_now.is_root: + depth=depth+1 + parent_key=syntax_now.parent_key + syntax_now=self.syntax_dict[parent_key] + parent_name=syntax_now.name + block.parent_blocks.insert(0,parent_name) + block.depth=depth + + return block + + def get_available_blocks(self): + available={} + for unique_key in self.syntax_dict.keys(): + available[unique_key]=self.make_block(unique_key) + return available + + # @property + # def root_keys(self): + # return [ unique_key for unique_key, syntax in self.syntax_dict.items() if syntax.is_root ] class SyntaxPath(): def __init__(self, syntax_path_in): @@ -54,7 +108,7 @@ def __init__(self, syntax_path_in): self.is_root=False self.parent_path=None self.parent_relation_path=None - self.child_paths=[] + self.child_paths={} self.path=deepcopy(syntax_path_in) @@ -112,6 +166,18 @@ def _get_lookup_key(self,path_in,name_in): lookup_path+=name_in return lookup_path + def add_child(self, child_syntax): + assert isinstance(child_syntax,SyntaxPath) + + # Save mapping by relation type + relation_key=self._key_from_list(child_syntax.parent_relation) + if relation_key not in self.child_paths.keys(): + self.child_paths[relation_key]=[] + self.child_paths[relation_key].append(child_syntax.unique_key) + + def has_child_type(self,relation): + return relation in self.child_paths.keys() and self.child_paths[relation] != None + @property def parent_key(self): if not self.is_root: @@ -122,44 +188,94 @@ def parent_key(self): else: return None - def add_child(self, child_syntax): - assert isinstance(child_syntax,SyntaxPath) - self.child_paths.append(child_syntax.unique_key) + # @property + # def has_type(self): + # return self.has_child_type("types") + + # @property + # def has_actions(self): + # return self.has_child_type("actions") + + # @property + # def has_systems(self): + # return self.has_child_type("systems") + + # use child relation to parent type + ## relation cases + # Handle these in SyntaxBlock + #['actions']-->has action + #['subblocks']-->has system + #['types']-->has type + #['star', 'actions']-->has action_collection + #['star', 'subblock_types']--> has typed_collection + #['star', 'subblocks']--> has system_collection class SyntaxBlock(): - def __init__(self, _name, _syntax_type, _known_types): - self.name=_name - self.syntax_type=_syntax_type + """ + A class to represent one block of MOOSE syntax + """ + def __init__(self): + self.name="" + self.has_params=False + self.available_syntax={} + self.parent_blocks=[] + self.depth=0 self.enabled=False - self.enabled_types={} - if _known_types is not None: - for known_type_name in _known_types: - self.enabled_types[known_type_name]=False - - # Store what the default type should be - self.default_type=None - - def to_dict(self): - syntax_dict={ - "name": self.name, - "syntax_type": self.syntax_type, - "enabled": self.enabled, - "enabled_types": self.enabled_types, - } - return syntax_dict - - def enable_from_dict(self,dict_in): - self.enabled=dict_in["enabled"] - self.enabled_types=dict_in["enabled_types"] + + def to_dict(self,verbose=False): + block_dict=None + if self.enabled or verbose: + block_dict={ + "name": self.name, + "enabled": self.enabled, + "params": self.has_params, + "available syntax": self.available_syntax, + "parents": self.parent_blocks, + } + return block_dict @property - def enabled_subblocks(self): - if self.enabled_types is not None: - enabled_type_list=[ type_name for type_name, enabled in self.enabled_types.items() if enabled ] - else: - enabled_type_list=None - return enabled_type_list + def is_leaf(self): + return self.available_syntax==None + + @property + def is_root(self): + return self.depth==0 + + + # def __init__(self, _name, _syntax_type, _known_types): + # self.name=_name + # self.syntax_type=_syntax_type + # self.enabled=False + # self.enabled_types={} + # if _known_types is not None: + # for known_type_name in _known_types: + # self.enabled_types[known_type_name]=False + + # # Store what the default type should be + # self.default_type=None + + # def to_dict(self): + # syntax_dict={ + # "name": self.name, + # "syntax_type": self.syntax_type, + # "enabled": self.enabled, + # "enabled_types": self.enabled_types, + # } + # return syntax_dict + + # def enable_from_dict(self,dict_in): + # self.enabled=dict_in["enabled"] + # self.enabled_types=dict_in["enabled_types"] + + # @property + # def enabled_subblocks(self): + # if self.enabled_types is not None: + # enabled_type_list=[ type_name for type_name, enabled in self.enabled_types.items() if enabled ] + # else: + # enabled_type_list=None + # return enabled_type_list # convenience function for converting types From 9456c5ed885846400e4ed6b8095edbaeaa1de506 Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Mon, 11 Dec 2023 09:43:09 +0000 Subject: [PATCH 19/62] construct syntax registry from an executable path --- catbird/cbird.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/catbird/cbird.py b/catbird/cbird.py index 9ffabbf..6607de4 100644 --- a/catbird/cbird.py +++ b/catbird/cbird.py @@ -24,11 +24,12 @@ } class SyntaxRegistry(): - def __init__(self, syntax_paths_in): + def __init__(self, exec_path_in): self.syntax_dict={} - - assert isinstance(syntax_paths_in,list) - for path_now in syntax_paths_in: + all_json=json_from_exec(exec_path_in) + syntax_paths=key_search_recurse(all_json,[],"parameters",20) + assert isinstance(syntax_paths,list) + for path_now in syntax_paths: self._recurse_path(path_now) From bdc3ed3db451ca67e2b516a4498679a919004025 Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Mon, 11 Dec 2023 13:04:30 +0000 Subject: [PATCH 20/62] WIP: update factory to use syntax registry --- catbird/cbird.py | 30 ++++-- catbird/model.py | 233 ++++++++++++++++++++++++++--------------------- 2 files changed, 151 insertions(+), 112 deletions(-) diff --git a/catbird/cbird.py b/catbird/cbird.py index 6607de4..9979d04 100644 --- a/catbird/cbird.py +++ b/catbird/cbird.py @@ -73,6 +73,7 @@ def make_block(self,syntax_key): block=SyntaxBlock() block.name=syntax.name + block.path=syntax_key block.has_params=syntax.has_params block.available_syntax=self.get_available_syntax(syntax_key) @@ -93,6 +94,8 @@ def make_block(self,syntax_key): def get_available_blocks(self): available={} for unique_key in self.syntax_dict.keys(): + + available[unique_key]=self.make_block(unique_key) return available @@ -218,23 +221,30 @@ class SyntaxBlock(): """ def __init__(self): self.name="" + self.path="" self.has_params=False + self.enabled=False self.available_syntax={} self.parent_blocks=[] self.depth=0 - self.enabled=False - def to_dict(self,verbose=False): - block_dict=None - if self.enabled or verbose: + def to_dict(self, print_depth=3, verbose=False): + config_entry=None + if ( self.enabled and self.depth < print_depth ) or verbose: block_dict={ "name": self.name, "enabled": self.enabled, - "params": self.has_params, - "available syntax": self.available_syntax, - "parents": self.parent_blocks, + #"params": self.has_params, } - return block_dict + #if verbose: + # if self.available_syntax: + # block_dict["available syntax"] = self.available_syntax + # if self.parent_blocks: + # block_dict["parents"] = self.parent_blocks + + config_entry= { self.path : block_dict } + + return config_entry @property def is_leaf(self): @@ -244,6 +254,10 @@ def is_leaf(self): def is_root(self): return self.depth==0 + def path_to_child(self,relation_type,child_name): + path=self.path+"/"+_relation_shorthands[relation_type]+child_name + return path + # def __init__(self, _name, _syntax_type, _known_types): # self.name=_name diff --git a/catbird/model.py b/catbird/model.py index b4c58b4..daf5e4c 100644 --- a/catbird/model.py +++ b/catbird/model.py @@ -1,43 +1,48 @@ -from .cbird import SyntaxBlock, parse_blocks, parse_blocks_types, read_json, write_json +from .cbird import SyntaxRegistry, SyntaxBlock, parse_blocks, parse_blocks_types, read_json, write_json from .collection import MOOSECollection class Factory(): - def __init__(self,json_obj,config_file=None): - self.available_blocks=parse_blocks(json_obj) + def __init__(self,exec_path,config_file=None): + self.registry=SyntaxRegistry(exec_path) + self.available_blocks=self.registry.get_available_blocks() self.constructors={} self.set_defaults() if config_file is not None: self.load_config(config_file) self.load_enabled_objects(json_obj) - def load_enabled_objects(self,json_obj): - for enabled_block in self.enabled_syntax: - enabled_types=self.available_blocks[enabled_block].enabled_subblocks - syntax_type=self.available_blocks[enabled_block].syntax_type - if syntax_type=="fundamental" or syntax_type=="nested": - self.constructors.update(parse_blocks_types(json_obj,enabled_block,category_names=enabled_types)) - else: - msg="Unsupported syntax type {}".format(syntax_type) - raise NotImplementedError(msg) - - def construct(self,obj_type,**kwargs): - obj=self.constructors[obj_type]() - - # Handle keyword arguments - for key, value in kwargs.items(): - if not hasattr(obj,key): - msg="Object type {} does not have attribute {}".format(obj_type,key) - raise RuntimeError() - setattr(obj, key, value) - - return obj - - def enable_syntax(self,syntax_name,default=None,enabled_types="all"): + def load_enabled_objects(self,json_obj): + raise NotImplementedError + # for enabled_block in self.enabled_syntax: + # enabled_types=self.available_blocks[enabled_block].enabled_subblocks + # syntax_type=self.available_blocks[enabled_block].syntax_type + # if syntax_type=="fundamental" or syntax_type=="nested": + # self.constructors.update(parse_blocks_types(json_obj,enabled_block,category_names=enabled_types)) + # else: + # msg="Unsupported syntax type {}".format(syntax_type) + # raise NotImplementedError(msg) + + # def construct(self,obj_type,**kwargs): + # obj=self.constructors[obj_type]() + + # # Handle keyword arguments + # for key, value in kwargs.items(): + # if not hasattr(obj,key): + # msg="Object type {} does not have attribute {}".format(obj_type,key) + # raise RuntimeError() + # setattr(obj, key, value) + + # return obj + + def enable_syntax(self,block_name,enable_dict=None): """ Configure what MOOSE syntax to enable. Objects with enabled syntax will be converted to Python classes. """ + # Construct full name + syntax_name="blocks/"+block_name + # Check syntax is known if syntax_name not in self.available_blocks.keys(): msg="Cannot enable unknown syntax {}".format(syntax_name) @@ -45,84 +50,101 @@ def enable_syntax(self,syntax_name,default=None,enabled_types="all"): # Enable top level block syntax self.available_blocks[syntax_name].enabled=True + block_now=self.available_blocks[syntax_name] # Enable sub-block types - enabled_type_list=list() - if isinstance(enabled_types,list): - enabled_type_list=enabled_types - elif enabled_types=="all": - enabled_type_list=list(self.available_blocks[syntax_name].enabled_types.keys()) - else: - msg="Invalid format for enabled_types, type = {}".format(type(enabled_types)) - raise RuntimeError(msg) - - # Ensure default is enabled - if default not in enabled_type_list and default is not None: - enabled_type_list.append(default) - - for current_type in enabled_type_list: - # Ensure valid - if current_type not in self.available_blocks[syntax_name].enabled_types.keys(): - msg="Unknown type {} for block {}".format(current_type,syntax_name) - raise KeyError(msg) - - self.available_blocks[syntax_name].enabled_types[current_type]=True - - # Set default for typed blocks - syntax_type = self.available_blocks[syntax_name].syntax_type - if default is not None: - self.available_blocks[syntax_name].default_type=default - elif syntax_type == "fundamental" or syntax_type == "nested": - msg="Please set a default for typed blocks" - raise RuntimeError(msg) - - - def write_config(self,filename): + available_sub_syntax=self.registry.get_available_syntax(syntax_name) + for syntax_shortname, syntax_list in available_sub_syntax.items(): + + # If provided, only enable user-specified types + if enable_dict and syntax_shortname not in enable_dict.keys(): + continue + + for syntax_item in syntax_list: + if enable_dict and syntax_item not in enable_dict[syntax_shortname]: + continue + + # Get longname and enable + enable_key=block_now.path_to_child(syntax_shortname,syntax_item) + self.available_blocks[enable_key].enabled=True + + # # Ensure default is enabled + # if default not in enabled_type_list and default is not None: + # enabled_type_list.append(default) + + # for current_type in enabled_type_list: + # # Ensure valid + # if current_type not in self.available_blocks[syntax_name].enabled_types.keys(): + # msg="Unknown type {} for block {}".format(current_type,syntax_name) + # raise KeyError(msg) + + # self.available_blocks[syntax_name].enabled_types[current_type]=True + + # # Set default for typed blocks + # syntax_type = self.available_blocks[syntax_name].syntax_type + # if default is not None: + # self.available_blocks[syntax_name].default_type=default + # elif syntax_type == "fundamental" or syntax_type == "nested": + # msg="Please set a default for typed blocks" + # raise RuntimeError(msg) + + def write_config(self,filename,print_depth=3,verbose=False): config_dict={} for block_name, block in self.available_blocks.items(): - config_dict[block_name]=block.to_dict() + config_entry=block.to_dict(print_depth,verbose) + if config_entry is not None: + config_dict.update(config_entry) write_json(config_dict,filename) def load_config(self,filename): - config_in=read_json(filename) - #read from file - for block_name, block_dict in config_in.items(): - self.available_blocks[block_name].enable_from_dict(block_dict) + raise NotImplementedError + # config_in=read_json(filename) + # #read from file + # for block_name, block_dict in config_in.items(): + # self.available_blocks[block_name].enable_from_dict(block_dict) @property def enabled_syntax(self): enabled=[ block_name for block_name, block in self.available_blocks.items() if block.enabled ] return enabled - @property - def fundamental_syntax(self): - return self.get_syntax_list("fundamental") + # @property + # def fundamental_syntax(self): + # return self.get_syntax_list("fundamental") - @property - def nested_syntax(self): - return self.get_syntax_list("nested") + # @property + # def nested_syntax(self): + # return self.get_syntax_list("nested") - @property - def systems_syntax(self): - return self.get_syntax_list("system") + # @property + # def systems_syntax(self): + # return self.get_syntax_list("system") - @property - def nested_systems_syntax(self): - return self.get_syntax_list("nested_system") + # @property + # def nested_systems_syntax(self): + # return self.get_syntax_list("nested_system") - def get_syntax_list(self,syntax_type_str): - syntax_list=[ block_name for block_name, block in self.available_blocks.items() if block.syntax_type==syntax_type_str] - return syntax_list + # def get_syntax_list(self,syntax_type_str): + # syntax_list=[ block_name for block_name, block in self.available_blocks.items() if block.syntax_type==syntax_type_str] + # return syntax_list def set_defaults(self): - self.enable_syntax("Mesh",default="FileMesh",enabled_types=["FileMesh","GeneratedMesh"]) - self.enable_syntax("Executioner",default="Steady",enabled_types=["Steady","Transient"]) - self.enable_syntax("Problem",default="FEProblem",enabled_types=["FEProblem"]) - self.enable_syntax("Variables",default="MooseVariable",enabled_types=["MooseVariable"]) + self.enable_syntax("Mesh") + self.enable_syntax("Executioner") + self.enable_syntax("Problem") + self.enable_syntax("Variables") + + # self.enable_syntax("Mesh",default="FileMesh",enabled_types=["FileMesh","GeneratedMesh"]) + # self.enable_syntax("Executioner",default="Steady",enabled_types=["Steady","Transient"]) + # self.enable_syntax("Problem",default="FEProblem",enabled_types=["FEProblem"]) + # self.enable_syntax("Variables",default="MooseVariable",enabled_types=["MooseVariable"]) class MooseModel(): def __init__(self,json_obj,config_file=None): self.moose_objects={} + + # To-do: change signature + # Create a factory for moose objects and possibly configure from file self.factory=Factory(json_obj,config_file) # Add attributes to this model with default assignments @@ -141,30 +163,33 @@ def add_category(self, category, category_type, syntax_name=""): msg="Invalid block type {}".format(category) raise RuntimeError(msg) - # First look up the syntax type - syntax_type=self.factory.available_blocks[category].syntax_type - - # How to add depends on syntax type - if syntax_type == "fundamental": - # If fundmantal, just add. We're done. - self.add_object(category,category_type) - elif syntax_type == "nested": - if not hasattr(self,category): - self.add_collection(category) - self.add_to_collection(category,category_type,syntax_name) - elif syntax_type == "system": - raise NotImplementedError() - elif syntax_type == "nested_system": - raise NotImplementedError() - else: - msg="Unhandled syntax type {}".format(syntax_type) - raise RuntimeError(msg) - # Object has been constructed, now just book-keeping - category_key=category.lower() - if category_key not in self.moose_objects.keys(): - self.moose_objects[category_key]=list() - self.moose_objects[category_key].append(category_type) + raise NotImplementedError + + # # First look up the syntax type + # syntax_type=self.factory.available_blocks[category].syntax_type + + # # How to add depends on syntax type + # if syntax_type == "fundamental": + # # If fundmantal, just add. We're done. + # self.add_object(category,category_type) + # elif syntax_type == "nested": + # if not hasattr(self,category): + # self.add_collection(category) + # self.add_to_collection(category,category_type,syntax_name) + # elif syntax_type == "system": + # raise NotImplementedError() + # elif syntax_type == "nested_system": + # raise NotImplementedError() + # else: + # msg="Unhandled syntax type {}".format(syntax_type) + # raise RuntimeError(msg) + + # # Object has been constructed, now just book-keeping + # category_key=category.lower() + # if category_key not in self.moose_objects.keys(): + # self.moose_objects[category_key]=list() + # self.moose_objects[category_key].append(category_type) def add_object(self,object_category,object_type,**kwargs): obj=self.factory.construct(object_type,**kwargs) From 82edb83bb3917aba742957c8aeffa932f7825c2e Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Mon, 11 Dec 2023 14:01:41 +0000 Subject: [PATCH 21/62] WIP: continue generalising factory to more syntax --- catbird/cbird.py | 154 ++++++++++++++++++++++++++++++++++----------- catbird/model.py | 160 ++++++++++++++++++----------------------------- 2 files changed, 179 insertions(+), 135 deletions(-) diff --git a/catbird/cbird.py b/catbird/cbird.py index 9979d04..252c488 100644 --- a/catbird/cbird.py +++ b/catbird/cbird.py @@ -24,11 +24,12 @@ } class SyntaxRegistry(): - def __init__(self, exec_path_in): - self.syntax_dict={} - all_json=json_from_exec(exec_path_in) + def __init__(self, all_json): + # Flatten highly nested json dict syntax_paths=key_search_recurse(all_json,[],"parameters",20) assert isinstance(syntax_paths,list) + + self.syntax_dict={} for path_now in syntax_paths: self._recurse_path(path_now) @@ -94,11 +95,22 @@ def make_block(self,syntax_key): def get_available_blocks(self): available={} for unique_key in self.syntax_dict.keys(): - - available[unique_key]=self.make_block(unique_key) return available + # def get_available_blocks_sorted_by_depth(self): + # available_by_depth={} + # for unique_key in self.syntax_dict.keys(): + # block_now=self.make_block(unique_key) + # depth_now=block_now.depth + # if depth_now not in available_by_depth.keys(): + # available_by_depth[depth_now]={} + # available_by_depth[depth_now][unique_key]=block_now + # return available_by_depth + + # def blocks_by_depth(self, request_depth): + # return [ unique_key for unique_key, syntax in self.syntax_dict.items() if syntax.depth == request_depth ] + # @property # def root_keys(self): # return [ unique_key for unique_key, syntax in self.syntax_dict.items() if syntax.is_root ] @@ -234,7 +246,7 @@ def to_dict(self, print_depth=3, verbose=False): block_dict={ "name": self.name, "enabled": self.enabled, - #"params": self.has_params, + "params": self.has_params, } #if verbose: # if self.available_syntax: @@ -258,7 +270,6 @@ def path_to_child(self,relation_type,child_name): path=self.path+"/"+_relation_shorthands[relation_type]+child_name return path - # def __init__(self, _name, _syntax_type, _known_types): # self.name=_name # self.syntax_type=_syntax_type @@ -280,10 +291,6 @@ def path_to_child(self,relation_type,child_name): # } # return syntax_dict - # def enable_from_dict(self,dict_in): - # self.enabled=dict_in["enabled"] - # self.enabled_types=dict_in["enabled_types"] - # @property # def enabled_subblocks(self): # if self.enabled_types is not None: @@ -301,10 +308,27 @@ def _convert_to_type(t, val): val = t(val) return val -def get_params(json_dict,syntax): +# def get_params(json_dict,syntax): +# assert isinstance(syntax,SyntaxPath) +# assert syntax.has_params +# key_list=deepcopy(syntax.path) +# dict_now=json_dict +# while len(key_list) > 0: +# key_now=key_list.pop(0) +# obj_now=dict_now[key_now] + +# assert isinstance(obj_now,dict) +# dict_now=deepcopy(obj_now) + +# params=dict_now["parameters"] +# return params + +def get_block(json_dict,syntax): assert isinstance(syntax,SyntaxPath) - assert syntax.has_params + key_list=deepcopy(syntax.path) + assert len(key_list) > 0 + dict_now=json_dict while len(key_list) > 0: key_now=key_list.pop(0) @@ -313,8 +337,16 @@ def get_params(json_dict,syntax): assert isinstance(obj_now,dict) dict_now=deepcopy(obj_now) - params=dict_now["parameters"] - return params + + try: + assert syntax.has_params + except AssertionError: + print(syntax.name) + print(dict_now.keys()) + raise AssertionError + + return dict_now + class MooseParam(): """ @@ -338,31 +370,31 @@ def __init__(self): def set_syntax_name(self,syntax_name): self._syntax_name=syntax_name - @classmethod - def set_syntax_type(cls,syntax_type): - cls._syntax_type=syntax_type + # @classmethod + # def set_syntax_type(cls,syntax_type): + # cls._syntax_type=syntax_type - @classmethod - def set_syntax_category(cls,syntax_category): - cls._syntax_category=syntax_category + # @classmethod + # def set_syntax_category(cls,syntax_category): + # cls._syntax_category=syntax_category - @property - def syntax_block_name(self): - if self.is_nested: - return self._syntax_name - else: - return self._syntax_category + # @property + # def syntax_block_name(self): + # if self.is_nested: + # return self._syntax_name + # else: + # return self._syntax_category - @property - def indent_level(self): - if self.is_nested: - return 2 - else: - return 1 + # @property + # def indent_level(self): + # if self.is_nested: + # return 2 + # else: + # return 1 - @property - def is_nested(self): - return self._syntax_type =="nested" or self._syntax_type=="nested_system" + # @property + # def is_nested(self): + # return self._syntax_type =="nested" or self._syntax_type=="nested_system" @property def moose_params(self): @@ -921,6 +953,56 @@ def get_block_types(json_obj,block_name): return syntax_type_to_block_types +def parse_block(json_obj,block_path): + # Available syntax for this block as dict + block=get_block(json_obj,block_path) + + # Create new subclass of Catbird with a name that matches the block + name=block_path.name + new_cls = type(name, (Catbird,), dict()) + + # Add parameters as attributes + params=block["parameters"] + for param_name, param_info in params.items(): + # Determine the type of the parameter + attr_types = tuple(type_mapping[t] for t in param_info['basic_type'].split(':')) + attr_type = attr_types[-1] + + if len(attr_types) > 1: + for t in attr_types[:-1]: + assert issubclass(t, Iterable) + ndim = len(attr_types) - 1 + else: + ndim = 0 + + # Set allowed values if present + allowed_values = None + if param_info['options']: + values = param_info['options'].split() + allowed_values = [_convert_to_type(attr_type, v) for v in values] + + # Apply the default value if provided + # TODO: default values need to be handled differently. They are replacing + # properties in the type definition as they are now + default = None + if 'default' in param_info.keys() and param_info['default'] != None and param_info['default'] != '': + if ndim == 0: + default = _convert_to_type(attr_type, param_info['default']) + else: + default = [_convert_to_type(attr_type, v) for v in param_info['default'].split()] + + # Add an attribute to the class instance for this parameter + new_cls.newattr(param_name, + attr_type, + description=param_info.get('description'), + default=default, + dim=ndim, + allowed_vals=allowed_values) + + + # Return our new class + return new_cls + def parse_blocks_types(json_obj,category,category_names=None): """ Make python objects out of MOOSE syntax for a fundamental category of block diff --git a/catbird/model.py b/catbird/model.py index daf5e4c..1237a00 100644 --- a/catbird/model.py +++ b/catbird/model.py @@ -1,38 +1,50 @@ -from .cbird import SyntaxRegistry, SyntaxBlock, parse_blocks, parse_blocks_types, read_json, write_json +from .cbird import SyntaxRegistry, SyntaxBlock, parse_block, read_json, write_json, json_from_exec from .collection import MOOSECollection class Factory(): def __init__(self,exec_path,config_file=None): - self.registry=SyntaxRegistry(exec_path) + json_obj=json_from_exec(exec_path) + self.registry=SyntaxRegistry(json_obj) self.available_blocks=self.registry.get_available_blocks() - self.constructors={} self.set_defaults() - if config_file is not None: - self.load_config(config_file) + # if config_file is not None: + # self.load_config(config_file) self.load_enabled_objects(json_obj) - def load_enabled_objects(self,json_obj): - raise NotImplementedError - # for enabled_block in self.enabled_syntax: - # enabled_types=self.available_blocks[enabled_block].enabled_subblocks - # syntax_type=self.available_blocks[enabled_block].syntax_type - # if syntax_type=="fundamental" or syntax_type=="nested": - # self.constructors.update(parse_blocks_types(json_obj,enabled_block,category_names=enabled_types)) - # else: - # msg="Unsupported syntax type {}".format(syntax_type) - # raise NotImplementedError(msg) - - # def construct(self,obj_type,**kwargs): - # obj=self.constructors[obj_type]() - - # # Handle keyword arguments - # for key, value in kwargs.items(): - # if not hasattr(obj,key): - # msg="Object type {} does not have attribute {}".format(obj_type,key) - # raise RuntimeError() - # setattr(obj, key, value) - - # return obj + def load_enabled_objects(self,json_obj): + self.constructors={} + for block_name, block in self.available_blocks.items(): + if not ( block.enabled and block.is_leaf ): + continue + + # Convert string to SyntaxPath + syntax_path=self.registry.syntax_dict[block_name] + + # Fetch syntax for block and make a new object type + new_class=parse_block(json_obj,syntax_path) + + # Some details about the type of object + class_name=syntax_path.name + namespace=syntax_path.parent_path[-1] + if namespace not in self.constructors.keys(): + self.constructors[namespace]={} + if class_name in self.constructors[namespace].keys(): + raise RuntimeError("Duplicated class name {} in namespace {}".format(class_name,namespace)) + + # Save class constructor + self.constructors[namespace][class_name]=new_class + + def construct(self,namespace,obj_type,**kwargs): + obj=self.constructors[namespace][obj_type]() + + # Handle keyword arguments + for key, value in kwargs.items(): + if not hasattr(obj,key): + msg="Object type {} does not have attribute {}".format(obj_type,key) + raise RuntimeError() + setattr(obj, key, value) + + return obj def enable_syntax(self,block_name,enable_dict=None): """ @@ -68,25 +80,6 @@ def enable_syntax(self,block_name,enable_dict=None): enable_key=block_now.path_to_child(syntax_shortname,syntax_item) self.available_blocks[enable_key].enabled=True - # # Ensure default is enabled - # if default not in enabled_type_list and default is not None: - # enabled_type_list.append(default) - - # for current_type in enabled_type_list: - # # Ensure valid - # if current_type not in self.available_blocks[syntax_name].enabled_types.keys(): - # msg="Unknown type {} for block {}".format(current_type,syntax_name) - # raise KeyError(msg) - - # self.available_blocks[syntax_name].enabled_types[current_type]=True - - # # Set default for typed blocks - # syntax_type = self.available_blocks[syntax_name].syntax_type - # if default is not None: - # self.available_blocks[syntax_name].default_type=default - # elif syntax_type == "fundamental" or syntax_type == "nested": - # msg="Please set a default for typed blocks" - # raise RuntimeError(msg) def write_config(self,filename,print_depth=3,verbose=False): config_dict={} @@ -97,36 +90,10 @@ def write_config(self,filename,print_depth=3,verbose=False): write_json(config_dict,filename) def load_config(self,filename): - raise NotImplementedError - # config_in=read_json(filename) - # #read from file - # for block_name, block_dict in config_in.items(): - # self.available_blocks[block_name].enable_from_dict(block_dict) - - @property - def enabled_syntax(self): - enabled=[ block_name for block_name, block in self.available_blocks.items() if block.enabled ] - return enabled - - # @property - # def fundamental_syntax(self): - # return self.get_syntax_list("fundamental") - - # @property - # def nested_syntax(self): - # return self.get_syntax_list("nested") - - # @property - # def systems_syntax(self): - # return self.get_syntax_list("system") - - # @property - # def nested_systems_syntax(self): - # return self.get_syntax_list("nested_system") - - # def get_syntax_list(self,syntax_type_str): - # syntax_list=[ block_name for block_name, block in self.available_blocks.items() if block.syntax_type==syntax_type_str] - # return syntax_list + # Fetch enabled objects from filename + config_in=read_json(filename) + for block_name, block_dict in config_in.items(): + self.available_blocks[block_name].enabled=block_dict["enabled"] def set_defaults(self): self.enable_syntax("Mesh") @@ -140,31 +107,25 @@ def set_defaults(self): # self.enable_syntax("Variables",default="MooseVariable",enabled_types=["MooseVariable"]) class MooseModel(): - def __init__(self,json_obj,config_file=None): - self.moose_objects={} - - # To-do: change signature + def __init__(self,factory_in): + assert isinstance(factory_in,Factory) + self.factory=factory_in - # Create a factory for moose objects and possibly configure from file - self.factory=Factory(json_obj,config_file) # Add attributes to this model with default assignments + self.moose_objects={} self.set_defaults() # Envisage this being overridden downstream. def set_defaults(self): - # Fundamental Types - self.add_category("Executioner", "Steady") - self.add_category("Problem", "FEProblem") - self.add_category("Mesh", "GeneratedMesh") - - def add_category(self, category, category_type, syntax_name=""): - # Ensure this is valid syntax - if category not in self.factory.enabled_syntax: - msg="Invalid block type {}".format(category) - raise RuntimeError(msg) - - - raise NotImplementedError + self.add_object("Executioner", "Steady") + self.add_object("Problem", "FEProblem") + self.add_object("Mesh", "GeneratedMesh") + + #def add_category(self, category, category_type, syntax_name=""): + # # Ensure this is valid syntax + # if category not in self.factory.constructors.keys(): + # msg="Invalid block type {}".format(category) + # raise RuntimeError(msg) # # First look up the syntax type # syntax_type=self.factory.available_blocks[category].syntax_type @@ -191,10 +152,10 @@ def add_category(self, category, category_type, syntax_name=""): # self.moose_objects[category_key]=list() # self.moose_objects[category_key].append(category_type) - def add_object(self,object_category,object_type,**kwargs): - obj=self.factory.construct(object_type,**kwargs) + def add_object(self,namespace,object_type,**kwargs): + obj=self.factory.construct(namespace,object_type,**kwargs) # Prefer non-capitalised attributes - attr_name=object_category.lower() + attr_name=namespace.lower() setattr(self,attr_name,obj) def add_collection(self, collection_type): @@ -224,7 +185,8 @@ def add_to_collection(self, collection_type, object_type, syntax_name, **kwargs) # Some short-hands for common operations def add_variable(self,name,variable_type="MooseVariable"): - self.add_category("Variables",variable_type,name) + raise NotImplementedError + #self.add_category("Variables",variable_type,name) def add_bc(self): raise NotImplementedError From e5dfdcb649c42e397301fafc54a26585a028991d Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Tue, 12 Dec 2023 10:39:19 +0000 Subject: [PATCH 22/62] refactoring - separate Factory class out --- catbird/factory.py | 101 +++++++++++++++++++++++++++++++++++ catbird/model.py | 127 +++++---------------------------------------- 2 files changed, 113 insertions(+), 115 deletions(-) create mode 100644 catbird/factory.py diff --git a/catbird/factory.py b/catbird/factory.py new file mode 100644 index 0000000..2d42d92 --- /dev/null +++ b/catbird/factory.py @@ -0,0 +1,101 @@ +from .cbird import SyntaxRegistry, parse_block, read_json, write_json, json_from_exec + +class Factory(): + def __init__(self,exec_path,config_file=None): + json_obj=json_from_exec(exec_path) + self.registry=SyntaxRegistry(json_obj) + self.available_blocks=self.registry.get_available_blocks() + self.set_defaults() + if config_file is not None: + self.load_config(config_file) + self.load_enabled_objects(json_obj) + + def load_enabled_objects(self,json_obj): + self.constructors={} + for block_name, block in self.available_blocks.items(): + if not ( block.enabled and block.is_leaf ): + continue + + # Convert string to SyntaxPath + syntax_path=self.registry.syntax_dict[block_name] + + # Fetch syntax for block and make a new object type + new_class=parse_block(json_obj,syntax_path) + + # Some details about the type of object + class_name=syntax_path.name + namespace=syntax_path.parent_path[-1] + if namespace not in self.constructors.keys(): + self.constructors[namespace]={} + if class_name in self.constructors[namespace].keys(): + raise RuntimeError("Duplicated class name {} in namespace {}".format(class_name,namespace)) + + # Save class constructor + self.constructors[namespace][class_name]=new_class + + def construct(self,namespace,obj_type,**kwargs): + obj=self.constructors[namespace][obj_type]() + + # Handle keyword arguments + for key, value in kwargs.items(): + if not hasattr(obj,key): + msg="Object type {} does not have attribute {}".format(obj_type,key) + raise RuntimeError() + setattr(obj, key, value) + + return obj + + def enable_syntax(self,block_name,enable_dict=None): + """ + Configure what MOOSE syntax to enable. + + Objects with enabled syntax will be converted to Python classes. + """ + # Construct full name + syntax_name="blocks/"+block_name + + # Check syntax is known + if syntax_name not in self.available_blocks.keys(): + msg="Cannot enable unknown syntax {}".format(syntax_name) + raise RuntimeError(msg) + + # Enable top level block syntax + self.available_blocks[syntax_name].enabled=True + block_now=self.available_blocks[syntax_name] + + # Enable sub-block types + available_sub_syntax=self.registry.get_available_syntax(syntax_name) + for syntax_shortname, syntax_list in available_sub_syntax.items(): + + # If provided, only enable user-specified types + if enable_dict and syntax_shortname not in enable_dict.keys(): + continue + + for syntax_item in syntax_list: + if enable_dict and syntax_item not in enable_dict[syntax_shortname]: + continue + + # Get longname and enable + enable_key=block_now.path_to_child(syntax_shortname,syntax_item) + self.available_blocks[enable_key].enabled=True + + + def write_config(self,filename,print_depth=3,verbose=False): + config_dict={} + for block_name, block in self.available_blocks.items(): + config_entry=block.to_dict(print_depth,verbose) + if config_entry is not None: + config_dict.update(config_entry) + write_json(config_dict,filename) + + def load_config(self,filename): + # Fetch enabled objects from filename + config_in=read_json(filename) + for block_name, block_dict in config_in.items(): + self.available_blocks[block_name].enabled=block_dict["enabled"] + + def set_defaults(self): + self.enable_syntax("Mesh") + self.enable_syntax("Executioner") + self.enable_syntax("Problem") + self.enable_syntax("Variables") diff --git a/catbird/model.py b/catbird/model.py index 1237a00..d8c10f2 100644 --- a/catbird/model.py +++ b/catbird/model.py @@ -1,110 +1,5 @@ -from .cbird import SyntaxRegistry, SyntaxBlock, parse_block, read_json, write_json, json_from_exec from .collection import MOOSECollection - -class Factory(): - def __init__(self,exec_path,config_file=None): - json_obj=json_from_exec(exec_path) - self.registry=SyntaxRegistry(json_obj) - self.available_blocks=self.registry.get_available_blocks() - self.set_defaults() - # if config_file is not None: - # self.load_config(config_file) - self.load_enabled_objects(json_obj) - - def load_enabled_objects(self,json_obj): - self.constructors={} - for block_name, block in self.available_blocks.items(): - if not ( block.enabled and block.is_leaf ): - continue - - # Convert string to SyntaxPath - syntax_path=self.registry.syntax_dict[block_name] - - # Fetch syntax for block and make a new object type - new_class=parse_block(json_obj,syntax_path) - - # Some details about the type of object - class_name=syntax_path.name - namespace=syntax_path.parent_path[-1] - if namespace not in self.constructors.keys(): - self.constructors[namespace]={} - if class_name in self.constructors[namespace].keys(): - raise RuntimeError("Duplicated class name {} in namespace {}".format(class_name,namespace)) - - # Save class constructor - self.constructors[namespace][class_name]=new_class - - def construct(self,namespace,obj_type,**kwargs): - obj=self.constructors[namespace][obj_type]() - - # Handle keyword arguments - for key, value in kwargs.items(): - if not hasattr(obj,key): - msg="Object type {} does not have attribute {}".format(obj_type,key) - raise RuntimeError() - setattr(obj, key, value) - - return obj - - def enable_syntax(self,block_name,enable_dict=None): - """ - Configure what MOOSE syntax to enable. - - Objects with enabled syntax will be converted to Python classes. - """ - # Construct full name - syntax_name="blocks/"+block_name - - # Check syntax is known - if syntax_name not in self.available_blocks.keys(): - msg="Cannot enable unknown syntax {}".format(syntax_name) - raise RuntimeError(msg) - - # Enable top level block syntax - self.available_blocks[syntax_name].enabled=True - block_now=self.available_blocks[syntax_name] - - # Enable sub-block types - available_sub_syntax=self.registry.get_available_syntax(syntax_name) - for syntax_shortname, syntax_list in available_sub_syntax.items(): - - # If provided, only enable user-specified types - if enable_dict and syntax_shortname not in enable_dict.keys(): - continue - - for syntax_item in syntax_list: - if enable_dict and syntax_item not in enable_dict[syntax_shortname]: - continue - - # Get longname and enable - enable_key=block_now.path_to_child(syntax_shortname,syntax_item) - self.available_blocks[enable_key].enabled=True - - - def write_config(self,filename,print_depth=3,verbose=False): - config_dict={} - for block_name, block in self.available_blocks.items(): - config_entry=block.to_dict(print_depth,verbose) - if config_entry is not None: - config_dict.update(config_entry) - write_json(config_dict,filename) - - def load_config(self,filename): - # Fetch enabled objects from filename - config_in=read_json(filename) - for block_name, block_dict in config_in.items(): - self.available_blocks[block_name].enabled=block_dict["enabled"] - - def set_defaults(self): - self.enable_syntax("Mesh") - self.enable_syntax("Executioner") - self.enable_syntax("Problem") - self.enable_syntax("Variables") - - # self.enable_syntax("Mesh",default="FileMesh",enabled_types=["FileMesh","GeneratedMesh"]) - # self.enable_syntax("Executioner",default="Steady",enabled_types=["Steady","Transient"]) - # self.enable_syntax("Problem",default="FEProblem",enabled_types=["FEProblem"]) - # self.enable_syntax("Variables",default="MooseVariable",enabled_types=["MooseVariable"]) +from .factory import Factory class MooseModel(): def __init__(self,factory_in): @@ -170,18 +65,20 @@ def add_collection(self, collection_type): setattr(self, attr_name, new_cls()) def add_to_collection(self, collection_type, object_type, syntax_name, **kwargs): - # Construct object - obj=self.factory.construct(object_type,**kwargs) - if syntax_name=="": - raise RuntimeError("Must supply syntax_name for nested syntax") + raise NotImplementedError + + # # Construct object + # obj=self.factory.construct(collection_type,object_type,**kwargs) + # if syntax_name=="": + # raise RuntimeError("Must supply syntax_name for nested syntax") - obj.set_syntax_name(syntax_name) + # obj.set_syntax_name(syntax_name) - # Obtain the object for this collection type - collection = getattr(self, collection_type.lower()) + # # Obtain the object for this collection type + # collection = getattr(self, collection_type.lower()) - # Store in collection - collection.add(obj) + # # Store in collection + # collection.add(obj) # Some short-hands for common operations def add_variable(self,name,variable_type="MooseVariable"): From 21fad0b2054a9e235f2e432846b7b76a13890e08 Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Tue, 12 Dec 2023 11:05:50 +0000 Subject: [PATCH 23/62] refactoring - separate Syntax classes out --- catbird/cbird.py | 338 +-------------------------------------------- catbird/factory.py | 3 +- catbird/syntax.py | 327 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 331 insertions(+), 337 deletions(-) create mode 100644 catbird/syntax.py diff --git a/catbird/cbird.py b/catbird/cbird.py index 252c488..35a5cc7 100644 --- a/catbird/cbird.py +++ b/catbird/cbird.py @@ -1,10 +1,11 @@ from abc import ABC from collections.abc import Iterable +from copy import deepcopy import json import numpy as np from pathlib import Path import subprocess -from copy import deepcopy +from .syntax import SyntaxPath type_mapping = {'Integer' : int, 'Boolean' : bool, @@ -13,293 +14,6 @@ 'String' : str, 'Array' : list} -_relation_syntax=["blocks","subblocks","actions","star","types","subblock_types"] -_relation_shorthands={ - "types": "types/", - "actions":"actions/", - "systems": "subblocks/", - "type collections" : "star/subblock_types/", - "action collections": "star/actions/", - "system collections": "star/subblocks/" -} - -class SyntaxRegistry(): - def __init__(self, all_json): - # Flatten highly nested json dict - syntax_paths=key_search_recurse(all_json,[],"parameters",20) - assert isinstance(syntax_paths,list) - - self.syntax_dict={} - for path_now in syntax_paths: - self._recurse_path(path_now) - - - def _recurse_path(self, path_in, children=None): - syntax=SyntaxPath(path_in) - self._add_syntax(syntax) - - # Add / update parents - if syntax.parent_key is not None: - if syntax.parent_key not in self.syntax_dict: - self._recurse_path(syntax.parent_path) - # Add current node to parent - self.syntax_dict[syntax.parent_key].add_child(syntax) - - def _add_syntax(self, syntax): - assert isinstance(syntax,SyntaxPath) - assert syntax.unique_key not in self.syntax_dict.keys() - self.syntax_dict[syntax.unique_key]=syntax - - def get_children_of_type(self,syntax_key,relation_type): - children=[] - parent=self.syntax_dict[syntax_key] - if parent.has_child_type(relation_type): - for child_path in parent.child_paths[relation_type]: - child=self.syntax_dict[child_path] - children.append(child.name) - return children - - def get_available_syntax(self,syntax_key): - available={} - for shortname,relation in _relation_shorthands.items(): - syntax_list=self.get_children_of_type(syntax_key,relation) - if len(syntax_list)>0: - available[shortname]=syntax_list - if len(available.keys()) == 0: - available=None - return available - - def make_block(self,syntax_key): - syntax=self.syntax_dict[syntax_key] - - block=SyntaxBlock() - block.name=syntax.name - block.path=syntax_key - block.has_params=syntax.has_params - block.available_syntax=self.get_available_syntax(syntax_key) - - if not syntax.is_root: - # Recurse until we find root - syntax_now=syntax - depth=0 - while not syntax_now.is_root: - depth=depth+1 - parent_key=syntax_now.parent_key - syntax_now=self.syntax_dict[parent_key] - parent_name=syntax_now.name - block.parent_blocks.insert(0,parent_name) - block.depth=depth - - return block - - def get_available_blocks(self): - available={} - for unique_key in self.syntax_dict.keys(): - available[unique_key]=self.make_block(unique_key) - return available - - # def get_available_blocks_sorted_by_depth(self): - # available_by_depth={} - # for unique_key in self.syntax_dict.keys(): - # block_now=self.make_block(unique_key) - # depth_now=block_now.depth - # if depth_now not in available_by_depth.keys(): - # available_by_depth[depth_now]={} - # available_by_depth[depth_now][unique_key]=block_now - # return available_by_depth - - # def blocks_by_depth(self, request_depth): - # return [ unique_key for unique_key, syntax in self.syntax_dict.items() if syntax.depth == request_depth ] - - # @property - # def root_keys(self): - # return [ unique_key for unique_key, syntax in self.syntax_dict.items() if syntax.is_root ] - -class SyntaxPath(): - def __init__(self, syntax_path_in): - # Initial values - self.name="" - self.unique_key="" - self.has_params=False - self.is_root=False - self.parent_path=None - self.parent_relation_path=None - self.child_paths={} - self.path=deepcopy(syntax_path_in) - - - syntax_path=deepcopy(syntax_path_in) - - # Type assertions - assert isinstance(syntax_path,list) - assert len(syntax_path)>1 - for key in syntax_path: - assert isinstance(key,str) - - # Check for parameters - pos_now=len(syntax_path)-1 - key_now = syntax_path.pop(pos_now) - if key_now == "parameters": - self.has_params=True - self.path.pop(pos_now) - pos_now=pos_now-1 - key_now = syntax_path.pop(pos_now) - - # Set object name - self.name=key_now - self.unique_key=self._get_lookup_key(syntax_path,key_now) - - if len(syntax_path) > 1 : - relation_path=[] - found_parent=False - while not found_parent and len(syntax_path) > 0: - pos_now=len(syntax_path)-1 - test_key=syntax_path.pop(pos_now) - if test_key in _relation_syntax: - relation_path.insert(0,test_key) - else: - found_parent=True - syntax_path.append(test_key) - - if not found_parent: - raise RuntimeError("Should not get here") - self.parent_relation=relation_path - self.parent_path=syntax_path - - else: - self.is_root=True - - - def _key_from_list(self,path_in): - path_str="" - for key in path_in: - path_str+=key - path_str+="/" - return path_str - - def _get_lookup_key(self,path_in,name_in): - lookup_path=self._key_from_list(path_in) - lookup_path+=name_in - return lookup_path - - def add_child(self, child_syntax): - assert isinstance(child_syntax,SyntaxPath) - - # Save mapping by relation type - relation_key=self._key_from_list(child_syntax.parent_relation) - if relation_key not in self.child_paths.keys(): - self.child_paths[relation_key]=[] - self.child_paths[relation_key].append(child_syntax.unique_key) - - def has_child_type(self,relation): - return relation in self.child_paths.keys() and self.child_paths[relation] != None - - @property - def parent_key(self): - if not self.is_root: - parent_path_now=deepcopy(self.parent_path) - parent_len=len(parent_path_now) - parent_name=parent_path_now.pop(parent_len-1) - return self._get_lookup_key(parent_path_now,parent_name) - else: - return None - - # @property - # def has_type(self): - # return self.has_child_type("types") - - # @property - # def has_actions(self): - # return self.has_child_type("actions") - - # @property - # def has_systems(self): - # return self.has_child_type("systems") - - # use child relation to parent type - ## relation cases - # Handle these in SyntaxBlock - #['actions']-->has action - #['subblocks']-->has system - #['types']-->has type - #['star', 'actions']-->has action_collection - #['star', 'subblock_types']--> has typed_collection - #['star', 'subblocks']--> has system_collection - - -class SyntaxBlock(): - """ - A class to represent one block of MOOSE syntax - """ - def __init__(self): - self.name="" - self.path="" - self.has_params=False - self.enabled=False - self.available_syntax={} - self.parent_blocks=[] - self.depth=0 - - def to_dict(self, print_depth=3, verbose=False): - config_entry=None - if ( self.enabled and self.depth < print_depth ) or verbose: - block_dict={ - "name": self.name, - "enabled": self.enabled, - "params": self.has_params, - } - #if verbose: - # if self.available_syntax: - # block_dict["available syntax"] = self.available_syntax - # if self.parent_blocks: - # block_dict["parents"] = self.parent_blocks - - config_entry= { self.path : block_dict } - - return config_entry - - @property - def is_leaf(self): - return self.available_syntax==None - - @property - def is_root(self): - return self.depth==0 - - def path_to_child(self,relation_type,child_name): - path=self.path+"/"+_relation_shorthands[relation_type]+child_name - return path - - # def __init__(self, _name, _syntax_type, _known_types): - # self.name=_name - # self.syntax_type=_syntax_type - # self.enabled=False - # self.enabled_types={} - # if _known_types is not None: - # for known_type_name in _known_types: - # self.enabled_types[known_type_name]=False - - # # Store what the default type should be - # self.default_type=None - - # def to_dict(self): - # syntax_dict={ - # "name": self.name, - # "syntax_type": self.syntax_type, - # "enabled": self.enabled, - # "enabled_types": self.enabled_types, - # } - # return syntax_dict - - # @property - # def enabled_subblocks(self): - # if self.enabled_types is not None: - # enabled_type_list=[ type_name for type_name, enabled in self.enabled_types.items() if enabled ] - # else: - # enabled_type_list=None - # return enabled_type_list - - # convenience function for converting types def _convert_to_type(t, val): if t == bool: @@ -308,21 +22,6 @@ def _convert_to_type(t, val): val = t(val) return val -# def get_params(json_dict,syntax): -# assert isinstance(syntax,SyntaxPath) -# assert syntax.has_params -# key_list=deepcopy(syntax.path) -# dict_now=json_dict -# while len(key_list) > 0: -# key_now=key_list.pop(0) -# obj_now=dict_now[key_now] - -# assert isinstance(obj_now,dict) -# dict_now=deepcopy(obj_now) - -# params=dict_now["parameters"] -# return params - def get_block(json_dict,syntax): assert isinstance(syntax,SyntaxPath) @@ -347,7 +46,6 @@ def get_block(json_dict,syntax): return dict_now - class MooseParam(): """ Class to contain all information about a MOOSE parameter @@ -730,38 +428,6 @@ def problems_from_json(json_file, problem_names=None): return out -def key_search_recurse(dict_in, test_path, key_test, level_stop=15): - """ - Parse blocks recursively until we hit the given key - """ - if not isinstance(dict_in,dict): - return list() - - if len(test_path) == level_stop: - return list() - - if key_test in dict_in.keys(): - # Success at leaf node! Found key, return path to here - success_path=deepcopy(test_path) - success_path.append(key_test) - return [ success_path ] - - success_paths=[] - for key_now, test_obj in dict_in.items(): - # Path to be tested - path_now=deepcopy(test_path) - path_now.append(key_now) - - paths_to_success=key_search_recurse(test_obj,path_now,key_test,level_stop) - - # If search fails, paths will be empty - # Otherwise add to our known list of success paths from this node - if len( paths_to_success ) != 0: - success_paths.extend(paths_to_success) - - return success_paths - - def parse_blocks(json_obj): """ Returns the a dictionary of block types corresponding to the MOOSE application described diff --git a/catbird/factory.py b/catbird/factory.py index 2d42d92..8512292 100644 --- a/catbird/factory.py +++ b/catbird/factory.py @@ -1,4 +1,5 @@ -from .cbird import SyntaxRegistry, parse_block, read_json, write_json, json_from_exec +from .syntax import SyntaxRegistry +from .cbird import parse_block, read_json, write_json, json_from_exec class Factory(): def __init__(self,exec_path,config_file=None): diff --git a/catbird/syntax.py b/catbird/syntax.py new file mode 100644 index 0000000..edebf55 --- /dev/null +++ b/catbird/syntax.py @@ -0,0 +1,327 @@ +from copy import deepcopy + +_relation_syntax=["blocks","subblocks","actions","star","types","subblock_types"] +_relation_shorthands={ + "types": "types/", + "actions":"actions/", + "systems": "subblocks/", + "type collections" : "star/subblock_types/", + "action collections": "star/actions/", + "system collections": "star/subblocks/" +} + +class SyntaxPath(): + """ + A helper class to store a path of keys from a nested dictionary structure. + """ + def __init__(self, syntax_path_in): + # Initial values + self.name="" + self.unique_key="" + self.has_params=False + self.is_root=False + self.parent_path=None + self.parent_relation_path=None + self.child_paths={} + self.path=deepcopy(syntax_path_in) + + + syntax_path=deepcopy(syntax_path_in) + + # Type assertions + assert isinstance(syntax_path,list) + assert len(syntax_path)>1 + for key in syntax_path: + assert isinstance(key,str) + + # Check for parameters + pos_now=len(syntax_path)-1 + key_now = syntax_path.pop(pos_now) + if key_now == "parameters": + self.has_params=True + self.path.pop(pos_now) + pos_now=pos_now-1 + key_now = syntax_path.pop(pos_now) + + # Set object name + self.name=key_now + self.unique_key=self._get_lookup_key(syntax_path,key_now) + + if len(syntax_path) > 1 : + relation_path=[] + found_parent=False + while not found_parent and len(syntax_path) > 0: + pos_now=len(syntax_path)-1 + test_key=syntax_path.pop(pos_now) + if test_key in _relation_syntax: + relation_path.insert(0,test_key) + else: + found_parent=True + syntax_path.append(test_key) + + if not found_parent: + raise RuntimeError("Should not get here") + self.parent_relation=relation_path + self.parent_path=syntax_path + + else: + self.is_root=True + + + def _key_from_list(self,path_in): + path_str="" + for key in path_in: + path_str+=key + path_str+="/" + return path_str + + def _get_lookup_key(self,path_in,name_in): + lookup_path=self._key_from_list(path_in) + lookup_path+=name_in + return lookup_path + + def add_child(self, child_syntax): + assert isinstance(child_syntax,SyntaxPath) + + # Save mapping by relation type + relation_key=self._key_from_list(child_syntax.parent_relation) + if relation_key not in self.child_paths.keys(): + self.child_paths[relation_key]=[] + self.child_paths[relation_key].append(child_syntax.unique_key) + + def has_child_type(self,relation): + return relation in self.child_paths.keys() and self.child_paths[relation] != None + + @property + def parent_key(self): + if not self.is_root: + parent_path_now=deepcopy(self.parent_path) + parent_len=len(parent_path_now) + parent_name=parent_path_now.pop(parent_len-1) + return self._get_lookup_key(parent_path_now,parent_name) + else: + return None + + # @property + # def has_type(self): + # return self.has_child_type("types") + + # @property + # def has_actions(self): + # return self.has_child_type("actions") + + # @property + # def has_systems(self): + # return self.has_child_type("systems") + + # use child relation to parent type + ## relation cases + # Handle these in SyntaxBlock + #['actions']-->has action + #['subblocks']-->has system + #['types']-->has type + #['star', 'actions']-->has action_collection + #['star', 'subblock_types']--> has typed_collection + #['star', 'subblocks']--> has system_collection + +class SyntaxBlock(): + """ + A class to represent one block of MOOSE syntax + """ + def __init__(self): + self.name="" + self.path="" + self.has_params=False + self.enabled=False + self.available_syntax={} + self.parent_blocks=[] + self.depth=0 + + def to_dict(self, print_depth=3, verbose=False): + config_entry=None + if ( self.enabled and self.depth < print_depth ) or verbose: + block_dict={ + "name": self.name, + "enabled": self.enabled, + "params": self.has_params, + } + #if verbose: + # if self.available_syntax: + # block_dict["available syntax"] = self.available_syntax + # if self.parent_blocks: + # block_dict["parents"] = self.parent_blocks + + config_entry= { self.path : block_dict } + + return config_entry + + @property + def is_leaf(self): + return self.available_syntax==None + + @property + def is_root(self): + return self.depth==0 + + def path_to_child(self,relation_type,child_name): + path=self.path+"/"+_relation_shorthands[relation_type]+child_name + return path + + # def __init__(self, _name, _syntax_type, _known_types): + # self.name=_name + # self.syntax_type=_syntax_type + # self.enabled=False + # self.enabled_types={} + # if _known_types is not None: + # for known_type_name in _known_types: + # self.enabled_types[known_type_name]=False + + # # Store what the default type should be + # self.default_type=None + + # def to_dict(self): + # syntax_dict={ + # "name": self.name, + # "syntax_type": self.syntax_type, + # "enabled": self.enabled, + # "enabled_types": self.enabled_types, + # } + # return syntax_dict + + # @property + # def enabled_subblocks(self): + # if self.enabled_types is not None: + # enabled_type_list=[ type_name for type_name, enabled in self.enabled_types.items() if enabled ] + # else: + # enabled_type_list=None + # return enabled_type_list + + +class SyntaxRegistry(): + """ + A class to store MOOSE syntax extracted from a highly nested dictionary in a flattened format. + + + Entries in the registry are stored in a dictionary with a unique key that maps onto a SyntaxPath object. A unique SyntaxPath may be parsed to identify relationships with child / parent paths to produce a SyntaxBlock that represents a block of syntax in a MOOSE input. + """ + def __init__(self, all_json): + # Flatten highly nested json dict + syntax_paths=key_search_recurse(all_json,[],"parameters",20) + assert isinstance(syntax_paths,list) + + self.syntax_dict={} + for path_now in syntax_paths: + self._recurse_path(path_now) + + def _recurse_path(self, path_in, children=None): + syntax=SyntaxPath(path_in) + self._add_syntax(syntax) + + # Add / update parents + if syntax.parent_key is not None: + if syntax.parent_key not in self.syntax_dict: + self._recurse_path(syntax.parent_path) + # Add current node to parent + self.syntax_dict[syntax.parent_key].add_child(syntax) + + def _add_syntax(self, syntax): + assert isinstance(syntax,SyntaxPath) + assert syntax.unique_key not in self.syntax_dict.keys() + self.syntax_dict[syntax.unique_key]=syntax + + def get_children_of_type(self,syntax_key,relation_type): + children=[] + parent=self.syntax_dict[syntax_key] + if parent.has_child_type(relation_type): + for child_path in parent.child_paths[relation_type]: + child=self.syntax_dict[child_path] + children.append(child.name) + return children + + def get_available_syntax(self,syntax_key): + available={} + for shortname,relation in _relation_shorthands.items(): + syntax_list=self.get_children_of_type(syntax_key,relation) + if len(syntax_list)>0: + available[shortname]=syntax_list + if len(available.keys()) == 0: + available=None + return available + + def make_block(self,syntax_key): + syntax=self.syntax_dict[syntax_key] + + block=SyntaxBlock() + block.name=syntax.name + block.path=syntax_key + block.has_params=syntax.has_params + block.available_syntax=self.get_available_syntax(syntax_key) + + if not syntax.is_root: + # Recurse until we find root + syntax_now=syntax + depth=0 + while not syntax_now.is_root: + depth=depth+1 + parent_key=syntax_now.parent_key + syntax_now=self.syntax_dict[parent_key] + parent_name=syntax_now.name + block.parent_blocks.insert(0,parent_name) + block.depth=depth + + return block + + def get_available_blocks(self): + available={} + for unique_key in self.syntax_dict.keys(): + available[unique_key]=self.make_block(unique_key) + return available + + # def get_available_blocks_sorted_by_depth(self): + # available_by_depth={} + # for unique_key in self.syntax_dict.keys(): + # block_now=self.make_block(unique_key) + # depth_now=block_now.depth + # if depth_now not in available_by_depth.keys(): + # available_by_depth[depth_now]={} + # available_by_depth[depth_now][unique_key]=block_now + # return available_by_depth + + # def blocks_by_depth(self, request_depth): + # return [ unique_key for unique_key, syntax in self.syntax_dict.items() if syntax.depth == request_depth ] + + # @property + # def root_keys(self): + # return [ unique_key for unique_key, syntax in self.syntax_dict.items() if syntax.is_root ] + + +def key_search_recurse(dict_in, test_path, key_test, level_stop=15): + """ + Parse blocks recursively until we hit the given key + """ + if not isinstance(dict_in,dict): + return list() + + if len(test_path) == level_stop: + return list() + + if key_test in dict_in.keys(): + # Success at leaf node! Found key, return path to here + success_path=deepcopy(test_path) + success_path.append(key_test) + return [ success_path ] + + success_paths=[] + for key_now, test_obj in dict_in.items(): + # Path to be tested + path_now=deepcopy(test_path) + path_now.append(key_now) + + paths_to_success=key_search_recurse(test_obj,path_now,key_test,level_stop) + + # If search fails, paths will be empty + # Otherwise add to our known list of success paths from this node + if len( paths_to_success ) != 0: + success_paths.extend(paths_to_success) + + return success_paths From a77fffc02b2829ac264bb959f6352a914117c19e Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Tue, 12 Dec 2023 11:18:28 +0000 Subject: [PATCH 24/62] refactoring - separate Syntax classes out --- catbird/cbird.py | 88 +++------------------------------------------- catbird/factory.py | 3 +- catbird/utils.py | 72 +++++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 84 deletions(-) create mode 100644 catbird/utils.py diff --git a/catbird/cbird.py b/catbird/cbird.py index 35a5cc7..7ace6a0 100644 --- a/catbird/cbird.py +++ b/catbird/cbird.py @@ -1,11 +1,9 @@ from abc import ABC from collections.abc import Iterable from copy import deepcopy -import json import numpy as np -from pathlib import Path -import subprocess from .syntax import SyntaxPath +from .utils import json_from_exec type_mapping = {'Integer' : int, 'Boolean' : bool, @@ -328,86 +326,15 @@ def print_me(self): print(attr_str) -def json_from_exec(exec): +def problems_from_json(json_obj, problem_names=None): """ Returns the Python objects corresponding to the MOOSE application described by the json file. Parameters ---------- - json_file : str, or Path - Either an open file handle, or a path to the json file. If `json` is a - dict, it is assumed this is a pre-parsed json object. - - Returns - ------- - dict - A dictionary of all MOOSE objects - """ - json_proc = subprocess.Popen([exec, '--json'], stdout=subprocess.PIPE) - json_str = '' - - # filter out the header and footer from the json data - while True: - line = json_proc.stdout.readline().decode() - if not line: - break - if '**START JSON DATA**' in line: - continue - if '**END JSON DATA**' in line: - continue - - json_str += line - - j_obj = json.loads(json_str) - - return j_obj - -def write_json(json_dict_out,name): - """ - Write a dictionary in JSON format - - Parameters - ---------- - json_dict_out : dict - name: str - Save as name.json - """ - json_output = json.dumps(json_dict_out, indent=4) - json_name=name - if json_name.find(".json") < 0 : - json_name = name+".json" - - with open(json_name, "w") as fh: - fh.write(json_output) - fh.write("\n") - print("Wrote to ",json_name) - - -def read_json(json_file): - """ - Load the contents of a JSON file into a dict. - - Parameters - ---------- - json_file: str - Name of JSON file - """ - json_dict = {} - with open(json_file) as handle: - json_dict = json.load(handle) - return json_dict - -def problems_from_json(json_file, problem_names=None): - """ - Returns the Python objects corresponding to the MOOSE application described - by the json file. - - Parameters - ---------- - json_file : dict, str, or Path - Either an open file handle, or a path to the json file. If `json` is a - dict, it is assumed this is a pre-parsed json object. + json_obj : dict + Pre-parsed json object containing MOOSE syntax problems : Iterable of str Set of problems to generate classes for @@ -417,15 +344,10 @@ def problems_from_json(json_file, problem_names=None): A dictionary of problem objects """ - if isinstance(json_file, dict): - json_obj = json_file - else: - json_obj = json.load(json_file) + assert isinstance(json_obj, dict) out = dict() - out['problems'] = parse_problems(json_obj, problem_names=problem_names) - return out def parse_blocks(json_obj): diff --git a/catbird/factory.py b/catbird/factory.py index 8512292..a29a3ea 100644 --- a/catbird/factory.py +++ b/catbird/factory.py @@ -1,5 +1,6 @@ from .syntax import SyntaxRegistry -from .cbird import parse_block, read_json, write_json, json_from_exec +from .cbird import parse_block +from .utils import read_json, write_json, json_from_exec class Factory(): def __init__(self,exec_path,config_file=None): diff --git a/catbird/utils.py b/catbird/utils.py new file mode 100644 index 0000000..28a4fe5 --- /dev/null +++ b/catbird/utils.py @@ -0,0 +1,72 @@ +import json +import subprocess + +def json_from_exec(exec): + """ + Returns the Python objects corresponding to the MOOSE application described + by the json file. + + Parameters + ---------- + json_file : str, or Path + Either an open file handle, or a path to the json file. If `json` is a + dict, it is assumed this is a pre-parsed json object. + + Returns + ------- + dict + A dictionary of all MOOSE objects + """ + json_proc = subprocess.Popen([exec, '--json'], stdout=subprocess.PIPE) + json_str = '' + + # filter out the header and footer from the json data + while True: + line = json_proc.stdout.readline().decode() + if not line: + break + if '**START JSON DATA**' in line: + continue + if '**END JSON DATA**' in line: + continue + + json_str += line + + j_obj = json.loads(json_str) + + return j_obj + +def write_json(json_dict_out,name): + """ + Write a dictionary in JSON format + + Parameters + ---------- + json_dict_out : dict + name: str + Save as name.json + """ + json_output = json.dumps(json_dict_out, indent=4) + json_name=name + if json_name.find(".json") < 0 : + json_name = name+".json" + + with open(json_name, "w") as fh: + fh.write(json_output) + fh.write("\n") + print("Wrote to ",json_name) + + +def read_json(json_file): + """ + Load the contents of a JSON file into a dict. + + Parameters + ---------- + json_file: str + Name of JSON file + """ + json_dict = {} + with open(json_file) as handle: + json_dict = json.load(handle) + return json_dict From d9e0c45e0ac4b876a2d7c32d059d3c5816f8f7c0 Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Tue, 12 Dec 2023 11:23:22 +0000 Subject: [PATCH 25/62] refactoring - move parsing functions from cbird to syntax --- catbird/cbird.py | 74 --------------------------------------------- catbird/factory.py | 3 +- catbird/syntax.py | 75 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 76 deletions(-) diff --git a/catbird/cbird.py b/catbird/cbird.py index 7ace6a0..4d29074 100644 --- a/catbird/cbird.py +++ b/catbird/cbird.py @@ -20,30 +20,6 @@ def _convert_to_type(t, val): val = t(val) return val -def get_block(json_dict,syntax): - assert isinstance(syntax,SyntaxPath) - - key_list=deepcopy(syntax.path) - assert len(key_list) > 0 - - dict_now=json_dict - while len(key_list) > 0: - key_now=key_list.pop(0) - obj_now=dict_now[key_now] - - assert isinstance(obj_now,dict) - dict_now=deepcopy(obj_now) - - - try: - assert syntax.has_params - except AssertionError: - print(syntax.name) - print(dict_now.keys()) - raise AssertionError - - return dict_now - class MooseParam(): """ Class to contain all information about a MOOSE parameter @@ -541,56 +517,6 @@ def get_block_types(json_obj,block_name): return syntax_type_to_block_types -def parse_block(json_obj,block_path): - # Available syntax for this block as dict - block=get_block(json_obj,block_path) - - # Create new subclass of Catbird with a name that matches the block - name=block_path.name - new_cls = type(name, (Catbird,), dict()) - - # Add parameters as attributes - params=block["parameters"] - for param_name, param_info in params.items(): - # Determine the type of the parameter - attr_types = tuple(type_mapping[t] for t in param_info['basic_type'].split(':')) - attr_type = attr_types[-1] - - if len(attr_types) > 1: - for t in attr_types[:-1]: - assert issubclass(t, Iterable) - ndim = len(attr_types) - 1 - else: - ndim = 0 - - # Set allowed values if present - allowed_values = None - if param_info['options']: - values = param_info['options'].split() - allowed_values = [_convert_to_type(attr_type, v) for v in values] - - # Apply the default value if provided - # TODO: default values need to be handled differently. They are replacing - # properties in the type definition as they are now - default = None - if 'default' in param_info.keys() and param_info['default'] != None and param_info['default'] != '': - if ndim == 0: - default = _convert_to_type(attr_type, param_info['default']) - else: - default = [_convert_to_type(attr_type, v) for v in param_info['default'].split()] - - # Add an attribute to the class instance for this parameter - new_cls.newattr(param_name, - attr_type, - description=param_info.get('description'), - default=default, - dim=ndim, - allowed_vals=allowed_values) - - - # Return our new class - return new_cls - def parse_blocks_types(json_obj,category,category_names=None): """ Make python objects out of MOOSE syntax for a fundamental category of block diff --git a/catbird/factory.py b/catbird/factory.py index a29a3ea..ed85d33 100644 --- a/catbird/factory.py +++ b/catbird/factory.py @@ -1,5 +1,4 @@ -from .syntax import SyntaxRegistry -from .cbird import parse_block +from .syntax import SyntaxRegistry, parse_block from .utils import read_json, write_json, json_from_exec class Factory(): diff --git a/catbird/syntax.py b/catbird/syntax.py index edebf55..da9616f 100644 --- a/catbird/syntax.py +++ b/catbird/syntax.py @@ -1,4 +1,5 @@ from copy import deepcopy +from .cbird import Catbird _relation_syntax=["blocks","subblocks","actions","star","types","subblock_types"] _relation_shorthands={ @@ -325,3 +326,77 @@ def key_search_recurse(dict_in, test_path, key_test, level_stop=15): success_paths.extend(paths_to_success) return success_paths + +def get_block(json_dict,syntax): + assert isinstance(syntax,SyntaxPath) + + key_list=deepcopy(syntax.path) + assert len(key_list) > 0 + + dict_now=json_dict + while len(key_list) > 0: + key_now=key_list.pop(0) + obj_now=dict_now[key_now] + + assert isinstance(obj_now,dict) + dict_now=deepcopy(obj_now) + + + try: + assert syntax.has_params + except AssertionError: + print(syntax.name) + print(dict_now.keys()) + raise AssertionError + + return dict_now + +def parse_block(json_obj,block_path): + # Available syntax for this block as dict + block=get_block(json_obj,block_path) + + # Create new subclass of Catbird with a name that matches the block + name=block_path.name + new_cls = type(name, (Catbird,), dict()) + + # Add parameters as attributes + params=block["parameters"] + for param_name, param_info in params.items(): + # Determine the type of the parameter + attr_types = tuple(type_mapping[t] for t in param_info['basic_type'].split(':')) + attr_type = attr_types[-1] + + if len(attr_types) > 1: + for t in attr_types[:-1]: + assert issubclass(t, Iterable) + ndim = len(attr_types) - 1 + else: + ndim = 0 + + # Set allowed values if present + allowed_values = None + if param_info['options']: + values = param_info['options'].split() + allowed_values = [_convert_to_type(attr_type, v) for v in values] + + # Apply the default value if provided + # TODO: default values need to be handled differently. They are replacing + # properties in the type definition as they are now + default = None + if 'default' in param_info.keys() and param_info['default'] != None and param_info['default'] != '': + if ndim == 0: + default = _convert_to_type(attr_type, param_info['default']) + else: + default = [_convert_to_type(attr_type, v) for v in param_info['default'].split()] + + # Add an attribute to the class instance for this parameter + new_cls.newattr(param_name, + attr_type, + description=param_info.get('description'), + default=default, + dim=ndim, + allowed_vals=allowed_values) + + + # Return our new class + return new_cls From 2e76a256bc1c6aa0d7f0803ef663dacee4f4cfa2 Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Tue, 12 Dec 2023 11:30:23 +0000 Subject: [PATCH 26/62] Add docstrings --- catbird/factory.py | 1 + catbird/model.py | 1 + catbird/syntax.py | 1 + catbird/utils.py | 1 + 4 files changed, 4 insertions(+) diff --git a/catbird/factory.py b/catbird/factory.py index ed85d33..78156d7 100644 --- a/catbird/factory.py +++ b/catbird/factory.py @@ -2,6 +2,7 @@ from .utils import read_json, write_json, json_from_exec class Factory(): + """Class to contain constructors for MOOSE syntax objects""" def __init__(self,exec_path,config_file=None): json_obj=json_from_exec(exec_path) self.registry=SyntaxRegistry(json_obj) diff --git a/catbird/model.py b/catbird/model.py index d8c10f2..ab17c06 100644 --- a/catbird/model.py +++ b/catbird/model.py @@ -2,6 +2,7 @@ from .factory import Factory class MooseModel(): + """Class to represent a MOOSE model""" def __init__(self,factory_in): assert isinstance(factory_in,Factory) self.factory=factory_in diff --git a/catbird/syntax.py b/catbird/syntax.py index da9616f..b28c532 100644 --- a/catbird/syntax.py +++ b/catbird/syntax.py @@ -1,3 +1,4 @@ +"""Classes and functions to parse MOOSE syntax""" from copy import deepcopy from .cbird import Catbird diff --git a/catbird/utils.py b/catbird/utils.py index 28a4fe5..2d684ad 100644 --- a/catbird/utils.py +++ b/catbird/utils.py @@ -1,3 +1,4 @@ +"""I/O utilities""" import json import subprocess From d5d6e7a233a5d7703f96b4555809c7f19200287d Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Tue, 12 Dec 2023 11:46:14 +0000 Subject: [PATCH 27/62] refactoring - move now obselete functions from cbird into legacy. hope to delete this later --- catbird/cbird.py | 345 ---------------------------------------------- catbird/legacy.py | 332 ++++++++++++++++++++++++++++++++++++++++++++ catbird/syntax.py | 15 ++ 3 files changed, 347 insertions(+), 345 deletions(-) create mode 100644 catbird/legacy.py diff --git a/catbird/cbird.py b/catbird/cbird.py index 4d29074..3d71f8a 100644 --- a/catbird/cbird.py +++ b/catbird/cbird.py @@ -3,22 +3,6 @@ from copy import deepcopy import numpy as np from .syntax import SyntaxPath -from .utils import json_from_exec - -type_mapping = {'Integer' : int, - 'Boolean' : bool, - 'Float' : float, - 'Real' : float, - 'String' : str, - 'Array' : list} - -# convenience function for converting types -def _convert_to_type(t, val): - if t == bool: - val = bool(int(val)) - else: - val = t(val) - return val class MooseParam(): """ @@ -301,332 +285,3 @@ def print_me(self): attr_str="{}.{}: None".format(name,attr_name) print(attr_str) - -def problems_from_json(json_obj, problem_names=None): - """ - Returns the Python objects corresponding to the MOOSE application described - by the json file. - - Parameters - ---------- - json_obj : dict - Pre-parsed json object containing MOOSE syntax - problems : Iterable of str - Set of problems to generate classes for - - Returns - ------- - dict - A dictionary of problem objects - """ - - assert isinstance(json_obj, dict) - - out = dict() - out['problems'] = parse_problems(json_obj, problem_names=problem_names) - return out - -def parse_blocks(json_obj): - """ - Returns the a dictionary of block types corresponding to the MOOSE application described - by the json file. - - Parameters - ---------- - json_obj : dict - Dictionary of full MOOSE object tree - - Returns - ------- - dict - Dictionary of available block types organised by category - """ - - # Get all top level categories of block - block_name_list = json_obj['blocks'].keys() - - #all_syntax=[] - parsed_blocks={} - - types_key='types' - wildcard_key='star' - nested_key='subblocks' - nested_block_key='subblock_types' - - for block_name in block_name_list: - block_dict_now = json_obj['blocks'][block_name] - if types_key in block_dict_now.keys(): - try : - # If dict - block_types_now = list(block_dict_now[types_key].keys()) - #fundamental_blocks[block_name]=block_types_now - parsed_blocks[block_name]=SyntaxBlock(block_name,"fundamental",block_types_now) - - #all_syntax.append(SyntaxBlock(block_name,"fundamental",block_types_now)) - except AttributeError : - # Otherwise - block_types_now = block_dict_now[types_key] - if block_types_now == None: - #systems.append(block_name) - parsed_blocks[block_name]=SyntaxBlock(block_name,"system",None) - #all_syntax.append(SyntaxBlock(block_name,"systems",None)) - continue - - #print(block_name," available types: ", block_types_now) - elif wildcard_key in block_dict_now.keys() and nested_block_key in block_dict_now[wildcard_key].keys(): - try: - types_now = list(block_dict_now[wildcard_key][nested_block_key].keys()) - #nested_blocks[block_name]=types_now - parsed_blocks[block_name]=SyntaxBlock(block_name,"nested",types_now) - #all_syntax.append(SyntaxBlock(block_name,"nested",types_now)) - - except AttributeError : - types_now = block_dict_now[wildcard_key][nested_block_key] - if types_now == None: - #nested_systems.append(block_name) - #all_syntax.append(SyntaxBlock(block_name,"nested_system",None)) - parsed_blocks[block_name]=SyntaxBlock(block_name,"nested_system",None) - continue - - elif nested_key in block_dict_now.keys(): - #nested_systems.append(block_name) - #all_syntax.append(SyntaxBlock(block_name,"nested_system",None)) - parsed_blocks[block_name]=SyntaxBlock(block_name,"nested_system",None) - - else: - print(block_name," has keys: ",block_dict_now.keys()) - raise RuntimeError("unhandled block category") - - - # parsed_block_list={} - # parsed_block_list["Systems"]=systems - # parsed_block_list["Nested systems"]=nested_systems - # parsed_block_list["Fundamental blocks"]=fundamental_blocks - # parsed_block_list["Nested blocks"]=nested_blocks - - #return parsed_block_list - return parsed_blocks - -def parse_problems(json_obj, problem_names=None): - return parse_blocks_types(json_obj,'Problem',category_names=problem_names) - -def get_block_types(json_obj,block_name): - block_types=None - syntax_type="" - - blocks_dict=json_obj['blocks'] - - if block_name not in blocks_dict.keys(): - msg="Unknown block name {}".format(block_name) - raise RuntimeError(msg) - - current_block_dict=blocks_dict[block_name] - - syntax_type_to_block_types={ - "fundamental":{}, - "system":{}, - "nested":{}, - "nested_system":{}, - "action":{}, - "double_nested":{}, - } - - # 6 cases, but not limited to single type at once - # TODO this is awful... refactor - # Suggest recursing down until found a "parameter" key - if 'types' in current_block_dict.keys() and current_block_dict['types'] is not None: - block_types=current_block_dict['types'] - syntax_type_to_block_types["fundamental"].update(block_types) - - if 'star' in current_block_dict.keys() and current_block_dict['star'] is not None: - if 'subblock_types' in current_block_dict['star'].keys(): - block_types=current_block_dict['star']['subblock_types'] - if block_types is not None: - syntax_type_to_block_types["nested"].update(block_types) - - if 'actions' in current_block_dict['star'].keys(): - block_types=current_block_dict['star']['actions'] - if block_types is not None: - syntax_type_to_block_types["nested_action"].update(block_types) - - if 'subblocks' in current_block_dict.keys() and current_block_dict['subblocks'] is not None: - - system_type_dict={} - nested_type_dict={} - double_nested_type_dict={} - - for subblock_name in current_block_dict['subblocks'].keys(): - subblock_dict=current_block_dict['subblocks'][subblock_name] - - if 'types' in subblock_dict.keys() and subblock_dict['types'] is not None: - block_types=subblock_dict['types'] - system_type_dict[subblock_name]=block_types - - if 'star' in subblock_dict.keys() and subblock_dict['star'] is not None: - if 'subblock_types' in subblock_dict['star'].keys(): - block_types=subblock_dict['star']['subblock_types'] - if block_types is not None: - nested_type_dict[subblock_name]=block_types - - if 'subblocks' in subblock_dict.keys() and subblock_dict['subblocks'] is not None: - - double_nested_type_dict[subblock_name]={} - - for subsubblock_name in subblock_dict['subblocks'].keys(): - subsubblock_dict=subblock_dict['subblocks'][subsubblock_name] - - if 'actions' in subsubblock_dict.keys() and subsubblock_dict['actions'] is not None: - double_nested_type_dict[subblock_name][subsubblock_name]=subsubblock_dict['actions'] - - if 'star' in subsubblock_dict.keys() and subsubblock_dict['star'] is not None: - if 'actions' in subsubblock_dict['star'].keys() and subsubblock_dict['star']['actions'] is not None: - double_nested_type_dict[subblock_name][subsubblock_name]=subsubblock_dict['star']['actions'] - - - if len(system_type_dict) >0: - syntax_type_to_block_types["system"].update(system_type_dict) - if len(nested_type_dict) >0: - syntax_type_to_block_types["nested_system"].update(nested_type_dict) - if len(double_nested_type_dict) >0: - syntax_type_to_block_types["double_nested"].update(double_nested_type_dict) - - - if 'actions' in current_block_dict.keys() and current_block_dict['actions'] is not None: - block_types=current_block_dict['actions'] - syntax_type_to_block_types["action"].update(block_types) - - - count_types=0 - for syntax_type in syntax_type_to_block_types.keys(): - block_types=syntax_type_to_block_types[syntax_type] - if len(block_types) > 0: - count_types+=1 - - if count_types == 0: - msg="Block {} is undocumented".format(block_name) - print(msg) - #raise RuntimeError(msg) - #block_types=None - #syntax_type="Unknown" - - elif count_types > 1: - msg="Block {} is has {} types".format(block_name,count_types) - print(msg) - - #return block_types, syntax_type - return syntax_type_to_block_types - - -def parse_blocks_types(json_obj,category,category_names=None): - """ - Make python objects out of MOOSE syntax for a fundamental category of block - (E.g. Executioner, Problem) - - Parameters - ---------- - json_obj : dict - A dictionary of all MOOSE objects - - category: str - A string naming the category of fundamental MOOSE block - - category_names: list(str) - Optional field. If provided, only return objects for specified types. - - Returns - ------- - dict - A dictionary of pythonised MOOSE objects of the given category. - """ - - requested_blocks,syntax_type = get_block_types(json_obj,category) - - instances_out = dict() - - for block_type, block_attributes in requested_blocks.items(): - # skip any blocks that we aren't looking for - if category_names is not None and block_type not in category_names: - continue - - # Todo add auto-documntations - #dict_keys(['description', 'file_info', 'label', 'moose_base', 'parameters', 'parent_syntax', 'register_file', 'syntax_path']) - - params = block_attributes['parameters'] - - # create new subclass of Catbird with a name that matches the block_type - new_cls = type(block_type, (Catbird,), dict()) - - # Set the - new_cls.set_syntax_type(syntax_type) - - if syntax_type != "nested": - new_cls.syntax_block_name=category - - # loop over the block_type parameters - for param_name, param_info in params.items(): - # determine the type of the parameter - attr_types = tuple(type_mapping[t] for t in param_info['basic_type'].split(':')) - attr_type = attr_types[-1] - - if len(attr_types) > 1: - for t in attr_types[:-1]: - assert issubclass(t, Iterable) - ndim = len(attr_types) - 1 - else: - ndim = 0 - - # set allowed values if present - allowed_values = None - if param_info['options']: - values = param_info['options'].split() - allowed_values = [_convert_to_type(attr_type, v) for v in values] - - # apply the default value if provided - # TODO: default values need to be handled differently. They are replacing - # properties in the type definition as they are now - default = None - if 'default' in param_info.keys() and param_info['default'] != None: - default = _convert_to_type(attr_type, param_info['default']) - # # only supporting defaults for one dimensional dim types - # vals = [_convert_to_type(attr_type, v) for v in param_info['default'].split()] - # if ndim == 0: - # default = vals[0] - # else: - # default = np.array(vals) - - # add an attribute to the class instance for this parameter - new_cls.newattr(param_name, - attr_type, - description=param_info.get('description'), - default=default, - dim=ndim, - allowed_vals=allowed_values) - - # insert new instance into the output dictionary - instances_out[block_type] = new_cls - - return instances_out - -def problem_from_exec(exec, problem_names=None): - """ - Returns the Python objects corresponding to the MOOSE - application described by the json file. - - Parameters - ---------- - problems : Iterable of str - Set of problems to generate classes for - - Returns - ------- - dict - A dictionary of problem objects - """ - j_obj = json_from_exec(exec) - - return problems_from_json(j_obj, problem_names=problem_names) - -def export_all_blocks_from_exec(exec,name): - j_obj = json_from_exec(exec) - block_dict=parse_blocks(j_obj) - write_json(block_dict,name) diff --git a/catbird/legacy.py b/catbird/legacy.py new file mode 100644 index 0000000..79b7739 --- /dev/null +++ b/catbird/legacy.py @@ -0,0 +1,332 @@ +from .cbird import Catbird +from .syntax import type_mapping, _convert_to_type +from .utils import json_from_exec, write_json + +def parse_blocks(json_obj): + """ + Returns the a dictionary of block types corresponding to the MOOSE application described + by the json file. + + Parameters + ---------- + json_obj : dict + Dictionary of full MOOSE object tree + + Returns + ------- + dict + Dictionary of available block types organised by category + """ + + # Get all top level categories of block + block_name_list = json_obj['blocks'].keys() + + #all_syntax=[] + parsed_blocks={} + + types_key='types' + wildcard_key='star' + nested_key='subblocks' + nested_block_key='subblock_types' + + for block_name in block_name_list: + block_dict_now = json_obj['blocks'][block_name] + if types_key in block_dict_now.keys(): + try : + # If dict + block_types_now = list(block_dict_now[types_key].keys()) + #fundamental_blocks[block_name]=block_types_now + parsed_blocks[block_name]=SyntaxBlock(block_name,"fundamental",block_types_now) + + #all_syntax.append(SyntaxBlock(block_name,"fundamental",block_types_now)) + except AttributeError : + # Otherwise + block_types_now = block_dict_now[types_key] + if block_types_now == None: + #systems.append(block_name) + parsed_blocks[block_name]=SyntaxBlock(block_name,"system",None) + #all_syntax.append(SyntaxBlock(block_name,"systems",None)) + continue + + #print(block_name," available types: ", block_types_now) + elif wildcard_key in block_dict_now.keys() and nested_block_key in block_dict_now[wildcard_key].keys(): + try: + types_now = list(block_dict_now[wildcard_key][nested_block_key].keys()) + #nested_blocks[block_name]=types_now + parsed_blocks[block_name]=SyntaxBlock(block_name,"nested",types_now) + #all_syntax.append(SyntaxBlock(block_name,"nested",types_now)) + + except AttributeError : + types_now = block_dict_now[wildcard_key][nested_block_key] + if types_now == None: + #nested_systems.append(block_name) + #all_syntax.append(SyntaxBlock(block_name,"nested_system",None)) + parsed_blocks[block_name]=SyntaxBlock(block_name,"nested_system",None) + continue + + elif nested_key in block_dict_now.keys(): + #nested_systems.append(block_name) + #all_syntax.append(SyntaxBlock(block_name,"nested_system",None)) + parsed_blocks[block_name]=SyntaxBlock(block_name,"nested_system",None) + + else: + print(block_name," has keys: ",block_dict_now.keys()) + raise RuntimeError("unhandled block category") + + + # parsed_block_list={} + # parsed_block_list["Systems"]=systems + # parsed_block_list["Nested systems"]=nested_systems + # parsed_block_list["Fundamental blocks"]=fundamental_blocks + # parsed_block_list["Nested blocks"]=nested_blocks + + #return parsed_block_list + return parsed_blocks + +def get_block_types(json_obj,block_name): + block_types=None + syntax_type="" + + blocks_dict=json_obj['blocks'] + + if block_name not in blocks_dict.keys(): + msg="Unknown block name {}".format(block_name) + raise RuntimeError(msg) + + current_block_dict=blocks_dict[block_name] + + syntax_type_to_block_types={ + "fundamental":{}, + "system":{}, + "nested":{}, + "nested_system":{}, + "action":{}, + "double_nested":{}, + } + + # 6 cases, but not limited to single type at once + # TODO this is awful... refactor + # Suggest recursing down until found a "parameter" key + if 'types' in current_block_dict.keys() and current_block_dict['types'] is not None: + block_types=current_block_dict['types'] + syntax_type_to_block_types["fundamental"].update(block_types) + + if 'star' in current_block_dict.keys() and current_block_dict['star'] is not None: + if 'subblock_types' in current_block_dict['star'].keys(): + block_types=current_block_dict['star']['subblock_types'] + if block_types is not None: + syntax_type_to_block_types["nested"].update(block_types) + + if 'actions' in current_block_dict['star'].keys(): + block_types=current_block_dict['star']['actions'] + if block_types is not None: + syntax_type_to_block_types["nested_action"].update(block_types) + + if 'subblocks' in current_block_dict.keys() and current_block_dict['subblocks'] is not None: + + system_type_dict={} + nested_type_dict={} + double_nested_type_dict={} + + for subblock_name in current_block_dict['subblocks'].keys(): + subblock_dict=current_block_dict['subblocks'][subblock_name] + + if 'types' in subblock_dict.keys() and subblock_dict['types'] is not None: + block_types=subblock_dict['types'] + system_type_dict[subblock_name]=block_types + + if 'star' in subblock_dict.keys() and subblock_dict['star'] is not None: + if 'subblock_types' in subblock_dict['star'].keys(): + block_types=subblock_dict['star']['subblock_types'] + if block_types is not None: + nested_type_dict[subblock_name]=block_types + + if 'subblocks' in subblock_dict.keys() and subblock_dict['subblocks'] is not None: + + double_nested_type_dict[subblock_name]={} + + for subsubblock_name in subblock_dict['subblocks'].keys(): + subsubblock_dict=subblock_dict['subblocks'][subsubblock_name] + + if 'actions' in subsubblock_dict.keys() and subsubblock_dict['actions'] is not None: + double_nested_type_dict[subblock_name][subsubblock_name]=subsubblock_dict['actions'] + + if 'star' in subsubblock_dict.keys() and subsubblock_dict['star'] is not None: + if 'actions' in subsubblock_dict['star'].keys() and subsubblock_dict['star']['actions'] is not None: + double_nested_type_dict[subblock_name][subsubblock_name]=subsubblock_dict['star']['actions'] + + + if len(system_type_dict) >0: + syntax_type_to_block_types["system"].update(system_type_dict) + if len(nested_type_dict) >0: + syntax_type_to_block_types["nested_system"].update(nested_type_dict) + if len(double_nested_type_dict) >0: + syntax_type_to_block_types["double_nested"].update(double_nested_type_dict) + + + if 'actions' in current_block_dict.keys() and current_block_dict['actions'] is not None: + block_types=current_block_dict['actions'] + syntax_type_to_block_types["action"].update(block_types) + + + count_types=0 + for syntax_type in syntax_type_to_block_types.keys(): + block_types=syntax_type_to_block_types[syntax_type] + if len(block_types) > 0: + count_types+=1 + + if count_types == 0: + msg="Block {} is undocumented".format(block_name) + print(msg) + #raise RuntimeError(msg) + #block_types=None + #syntax_type="Unknown" + + elif count_types > 1: + msg="Block {} is has {} types".format(block_name,count_types) + print(msg) + + #return block_types, syntax_type + return syntax_type_to_block_types + + +def parse_blocks_types(json_obj,category,category_names=None): + """ + Make python objects out of MOOSE syntax for a fundamental category of block + (E.g. Executioner, Problem) + + Parameters + ---------- + json_obj : dict + A dictionary of all MOOSE objects + + category: str + A string naming the category of fundamental MOOSE block + + category_names: list(str) + Optional field. If provided, only return objects for specified types. + + Returns + ------- + dict + A dictionary of pythonised MOOSE objects of the given category. + """ + + requested_blocks,syntax_type = get_block_types(json_obj,category) + + instances_out = dict() + + for block_type, block_attributes in requested_blocks.items(): + # skip any blocks that we aren't looking for + if category_names is not None and block_type not in category_names: + continue + + # Todo add auto-documntations + #dict_keys(['description', 'file_info', 'label', 'moose_base', 'parameters', 'parent_syntax', 'register_file', 'syntax_path']) + + params = block_attributes['parameters'] + + # create new subclass of Catbird with a name that matches the block_type + new_cls = type(block_type, (Catbird,), dict()) + + # Set the + new_cls.set_syntax_type(syntax_type) + + if syntax_type != "nested": + new_cls.syntax_block_name=category + + # loop over the block_type parameters + for param_name, param_info in params.items(): + # determine the type of the parameter + attr_types = tuple(type_mapping[t] for t in param_info['basic_type'].split(':')) + attr_type = attr_types[-1] + + if len(attr_types) > 1: + for t in attr_types[:-1]: + assert issubclass(t, Iterable) + ndim = len(attr_types) - 1 + else: + ndim = 0 + + # set allowed values if present + allowed_values = None + if param_info['options']: + values = param_info['options'].split() + allowed_values = [_convert_to_type(attr_type, v) for v in values] + + # apply the default value if provided + # TODO: default values need to be handled differently. They are replacing + # properties in the type definition as they are now + default = None + if 'default' in param_info.keys() and param_info['default'] != None: + default = _convert_to_type(attr_type, param_info['default']) + # # only supporting defaults for one dimensional dim types + # vals = [_convert_to_type(attr_type, v) for v in param_info['default'].split()] + # if ndim == 0: + # default = vals[0] + # else: + # default = np.array(vals) + + # add an attribute to the class instance for this parameter + new_cls.newattr(param_name, + attr_type, + description=param_info.get('description'), + default=default, + dim=ndim, + allowed_vals=allowed_values) + + # insert new instance into the output dictionary + instances_out[block_type] = new_cls + + return instances_out + +def parse_problems(json_obj, problem_names=None): + return parse_blocks_types(json_obj,'Problem',category_names=problem_names) + +def problems_from_json(json_obj, problem_names=None): + """ + Returns the Python objects corresponding to the MOOSE application described + by the json file. + + Parameters + ---------- + json_obj : dict + Pre-parsed json object containing MOOSE syntax + problems : Iterable of str + Set of problems to generate classes for + + Returns + ------- + dict + A dictionary of problem objects + """ + + assert isinstance(json_obj, dict) + + out = dict() + out['problems'] = parse_problems(json_obj, problem_names=problem_names) + return out + +def problem_from_exec(exec, problem_names=None): + """ + Returns the Python objects corresponding to the MOOSE + application described by the json file. + + Parameters + ---------- + problems : Iterable of str + Set of problems to generate classes for + + Returns + ------- + dict + A dictionary of problem objects + """ + j_obj = json_from_exec(exec) + + return problems_from_json(j_obj, problem_names=problem_names) + +def export_all_blocks_from_exec(exec,name): + j_obj = json_from_exec(exec) + block_dict=parse_blocks(j_obj) + write_json(block_dict,name) diff --git a/catbird/syntax.py b/catbird/syntax.py index b28c532..3036dfa 100644 --- a/catbird/syntax.py +++ b/catbird/syntax.py @@ -2,6 +2,13 @@ from copy import deepcopy from .cbird import Catbird +type_mapping = {'Integer' : int, + 'Boolean' : bool, + 'Float' : float, + 'Real' : float, + 'String' : str, + 'Array' : list} + _relation_syntax=["blocks","subblocks","actions","star","types","subblock_types"] _relation_shorthands={ "types": "types/", @@ -401,3 +408,11 @@ def parse_block(json_obj,block_path): # Return our new class return new_cls + +# convenience function for converting types +def _convert_to_type(t, val): + if t == bool: + val = bool(int(val)) + else: + val = t(val) + return val From 907a3e5fc8228f1cb06323b1c87af3cf38e4ed91 Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Tue, 12 Dec 2023 16:14:08 +0000 Subject: [PATCH 28/62] now support mixins in moose model basic syntax --- catbird/action.py | 3 +++ catbird/cbird.py | 2 -- catbird/collection.py | 36 +++++++++++++++++++++++++++++---- catbird/factory.py | 47 +++++++++++++++++++++++++++++++++++++++---- catbird/model.py | 3 +++ catbird/syntax.py | 25 +++++++++++++++++++++-- catbird/system.py | 3 +++ 7 files changed, 107 insertions(+), 12 deletions(-) create mode 100644 catbird/action.py create mode 100644 catbird/system.py diff --git a/catbird/action.py b/catbird/action.py new file mode 100644 index 0000000..f5c5074 --- /dev/null +++ b/catbird/action.py @@ -0,0 +1,3 @@ +class MOOSEAction(): + def __init__(self): + pass diff --git a/catbird/cbird.py b/catbird/cbird.py index 3d71f8a..4364fe9 100644 --- a/catbird/cbird.py +++ b/catbird/cbird.py @@ -1,8 +1,6 @@ from abc import ABC -from collections.abc import Iterable from copy import deepcopy import numpy as np -from .syntax import SyntaxPath class MooseParam(): """ diff --git a/catbird/collection.py b/catbird/collection.py index d1dc1d4..ae94691 100644 --- a/catbird/collection.py +++ b/catbird/collection.py @@ -1,10 +1,13 @@ from collections.abc import MutableSet +from .action import MOOSEAction from .cbird import Catbird +from .system import MOOSESystem class MOOSECollection(MutableSet): - """A collection of MOOSE (Catbird) objects""" + """A collection of MOOSE objects""" def __init__(self): self.objects={} + self._type=None # Define mandatory methods def __contains__(self,key): @@ -16,12 +19,14 @@ def __iter__(self): def __len__(self): return len(self.objects) + def _check_type(self,obj): + pass + def add(self,obj): - # Ensure type inherits from Catbird - assert issubclass(type(obj),Catbird) + # Type checking on object, raise an error if fails + self._check_type(obj) block_name=obj.syntax_block_name - if block_name in self.objects.keys(): msg="Collection already contains named block {}".format(block_name) raise RuntimeError(msg) @@ -38,3 +43,26 @@ def to_str(self,print_default=False): collection_str+=obj.to_str(print_default) collection_str+="[]\n" return collection_str + + +class MOOSEObjectCollection(MOOSECollection): + def __init__(self): + super().__init__() + + def _check_type(self,obj): + assert issubclass(type(obj),Catbird) + + +class MOOSEActionCollection(MOOSECollection): + def __init__(self): + super().__init__() + + def _check_type(self,obj): + assert issubclass(type(obj),MOOSEAction) + +class MOOSESystemCollection(MOOSECollection): + def __init__(self): + super().__init__() + + def _check_type(self,obj): + assert issubclass(type(obj),MOOSEAction) diff --git a/catbird/factory.py b/catbird/factory.py index 78156d7..92cf680 100644 --- a/catbird/factory.py +++ b/catbird/factory.py @@ -1,5 +1,6 @@ from .syntax import SyntaxRegistry, parse_block from .utils import read_json, write_json, json_from_exec +from .cbird import Catbird class Factory(): """Class to contain constructors for MOOSE syntax objects""" @@ -8,12 +9,22 @@ def __init__(self,exec_path,config_file=None): self.registry=SyntaxRegistry(json_obj) self.available_blocks=self.registry.get_available_blocks() self.set_defaults() - if config_file is not None: - self.load_config(config_file) + # if config_file is not None: + # self.load_config(config_file) + self.load_namespaces() self.load_enabled_objects(json_obj) + def load_namespaces(self): + # Loop over enabled root nodes + self.namespaces={} + for block_name, block in self.available_blocks.items(): + if not ( block.enabled and block.is_root ): + continue + self.namespaces[block.name]=block.get_mixins() + def load_enabled_objects(self,json_obj): self.constructors={} + # Loop over enabled leaf nodes for block_name, block in self.available_blocks.items(): if not ( block.enabled and block.is_leaf ): continue @@ -35,8 +46,36 @@ def load_enabled_objects(self,json_obj): # Save class constructor self.constructors[namespace][class_name]=new_class - def construct(self,namespace,obj_type,**kwargs): - obj=self.constructors[namespace][obj_type]() + @staticmethod + def __get_init_method(mixins): + # Define an __init__ function for the class. + def __init__(self, *args, **kwargs): + # Call the __init__ functions of all the mix-ins + for base in mixins: + base.__init__(self, *args, **kwargs) + return __init__ + + + def derive_class(self,namespace,obj_types): + # Get mixins boilerplate + mixins=self.namespaces[namespace] + + # Update mixins list by comparing types + for obj_type in obj_types: + class_now=self.constructors[namespace][obj_type] + for i_mixin, mixin in enumerate(mixins): + if issubclass(class_now,mixin): + mixins[i_mixin]=class_now + break + + # Our fancy new mixin class + new_cls = type(namespace, tuple(mixins),{"__init__":self.__get_init_method(mixins)}) + return new_cls + + def construct(self,namespace,*obj_types,**kwargs): + # Get class + obj_class=self.derive_class(namespace, obj_types) + obj=obj_class() # Handle keyword arguments for key, value in kwargs.items(): diff --git a/catbird/model.py b/catbird/model.py index ab17c06..a6bac89 100644 --- a/catbird/model.py +++ b/catbird/model.py @@ -49,6 +49,9 @@ def set_defaults(self): # self.moose_objects[category_key].append(category_type) def add_object(self,namespace,object_type,**kwargs): + """ + Add a MOOSE object from a given namespace to the model. + """ obj=self.factory.construct(namespace,object_type,**kwargs) # Prefer non-capitalised attributes attr_name=namespace.lower() diff --git a/catbird/syntax.py b/catbird/syntax.py index 3036dfa..d2f2987 100644 --- a/catbird/syntax.py +++ b/catbird/syntax.py @@ -1,6 +1,10 @@ """Classes and functions to parse MOOSE syntax""" +from collections.abc import Iterable from copy import deepcopy from .cbird import Catbird +from .action import MOOSEAction +from .system import MOOSESystem +from .collection import MOOSECollection type_mapping = {'Integer' : int, 'Boolean' : bool, @@ -19,6 +23,16 @@ "system collections": "star/subblocks/" } +_mixin_map={ + "types": Catbird, + "actions": MOOSEAction, + "systems": MOOSESystem, + "type collections" : MOOSECollection, + "action collections": MOOSECollection, + "system collections": MOOSECollection +} + + class SyntaxPath(): """ A helper class to store a path of keys from a nested dictionary structure. @@ -176,6 +190,14 @@ def path_to_child(self,relation_type,child_name): path=self.path+"/"+_relation_shorthands[relation_type]+child_name return path + def get_mixins(self): + mixin_list=[] + for relation_type in self.available_syntax.keys(): + mixin_now=_mixin_map[relation_type] + if mixin_now not in mixin_list: + mixin_list.append(mixin_now) + return mixin_list + # def __init__(self, _name, _syntax_type, _known_types): # self.name=_name # self.syntax_type=_syntax_type @@ -347,8 +369,7 @@ def get_block(json_dict,syntax): obj_now=dict_now[key_now] assert isinstance(obj_now,dict) - dict_now=deepcopy(obj_now) - + dict_now=deepcopy(obj_now) try: assert syntax.has_params diff --git a/catbird/system.py b/catbird/system.py new file mode 100644 index 0000000..65e721d --- /dev/null +++ b/catbird/system.py @@ -0,0 +1,3 @@ +class MOOSESystem(): + def __init__(self): + pass From 65e3f819aa2b0bb84b71039425d6d1daf6abe067 Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Tue, 12 Dec 2023 17:01:32 +0000 Subject: [PATCH 29/62] More consistent naming schema --- catbird/action.py | 2 +- catbird/cbird.py | 4 +- catbird/collection.py | 22 +++++----- catbird/factory.py | 1 - catbird/legacy.py | 8 ++-- catbird/model.py | 93 +++++++++++++++++++++---------------------- catbird/syntax.py | 30 +++++++------- catbird/system.py | 2 +- 8 files changed, 79 insertions(+), 83 deletions(-) diff --git a/catbird/action.py b/catbird/action.py index f5c5074..bf89d42 100644 --- a/catbird/action.py +++ b/catbird/action.py @@ -1,3 +1,3 @@ -class MOOSEAction(): +class MooseAction(): def __init__(self): pass diff --git a/catbird/cbird.py b/catbird/cbird.py index 4364fe9..5bdc5bb 100644 --- a/catbird/cbird.py +++ b/catbird/cbird.py @@ -14,9 +14,9 @@ def __init__(self): self.dim=0 self.doc="" -class Catbird(ABC): +class MooseObject(ABC): """ - Class to represent MOOSE syntax that can add type-checked properties to itself. + Class to represent typed MOOSE syntax that can add type-checked properties to itself. """ def __init__(self): self._syntax_name="" diff --git a/catbird/collection.py b/catbird/collection.py index ae94691..ede3b23 100644 --- a/catbird/collection.py +++ b/catbird/collection.py @@ -1,9 +1,9 @@ from collections.abc import MutableSet -from .action import MOOSEAction -from .cbird import Catbird -from .system import MOOSESystem +from .action import MooseAction +from .cbird import MooseObject +from .system import MooseSystem -class MOOSECollection(MutableSet): +class MooseCollection(MutableSet): """A collection of MOOSE objects""" def __init__(self): self.objects={} @@ -44,25 +44,23 @@ def to_str(self,print_default=False): collection_str+="[]\n" return collection_str - -class MOOSEObjectCollection(MOOSECollection): +class MooseObjectCollection(MooseCollection): def __init__(self): super().__init__() def _check_type(self,obj): - assert issubclass(type(obj),Catbird) - + assert issubclass(type(obj),MooseObject) -class MOOSEActionCollection(MOOSECollection): +class MooseActionCollection(MooseCollection): def __init__(self): super().__init__() def _check_type(self,obj): - assert issubclass(type(obj),MOOSEAction) + assert issubclass(type(obj),MooseAction) -class MOOSESystemCollection(MOOSECollection): +class MooseSystemCollection(MooseCollection): def __init__(self): super().__init__() def _check_type(self,obj): - assert issubclass(type(obj),MOOSEAction) + assert issubclass(type(obj),MooseSystem) diff --git a/catbird/factory.py b/catbird/factory.py index 92cf680..17507ee 100644 --- a/catbird/factory.py +++ b/catbird/factory.py @@ -1,6 +1,5 @@ from .syntax import SyntaxRegistry, parse_block from .utils import read_json, write_json, json_from_exec -from .cbird import Catbird class Factory(): """Class to contain constructors for MOOSE syntax objects""" diff --git a/catbird/legacy.py b/catbird/legacy.py index 79b7739..1b1110b 100644 --- a/catbird/legacy.py +++ b/catbird/legacy.py @@ -1,4 +1,4 @@ -from .cbird import Catbird +from .cbird import MooseObject from .syntax import type_mapping, _convert_to_type from .utils import json_from_exec, write_json @@ -226,10 +226,10 @@ def parse_blocks_types(json_obj,category,category_names=None): params = block_attributes['parameters'] - # create new subclass of Catbird with a name that matches the block_type - new_cls = type(block_type, (Catbird,), dict()) + # Create new subclass of MooseObject with a name that matches the block_type + new_cls = type(block_type, (MooseObject,), dict()) - # Set the + # Set the block title new_cls.set_syntax_type(syntax_type) if syntax_type != "nested": diff --git a/catbird/model.py b/catbird/model.py index a6bac89..00d3775 100644 --- a/catbird/model.py +++ b/catbird/model.py @@ -1,4 +1,3 @@ -from .collection import MOOSECollection from .factory import Factory class MooseModel(): @@ -57,53 +56,53 @@ def add_object(self,namespace,object_type,**kwargs): attr_name=namespace.lower() setattr(self,attr_name,obj) - def add_collection(self, collection_type): - # E.g. Variables, Kernels, BCs, Materials - # Create new subclass of with a name that matches the collection_type - new_cls = type(collection_type, (MOOSECollection,), dict()) + # def add_collection(self, collection_type): + # # E.g. Variables, Kernels, BCs, Materials + # # Create new subclass of with a name that matches the collection_type + # new_cls = type(collection_type, (MOOSECollection,), dict()) - # Prefer non-capitalised attributes - attr_name=collection_type.lower() + # # Prefer non-capitalised attributes + # attr_name=collection_type.lower() - # Construct and add the to model - setattr(self, attr_name, new_cls()) + # # Construct and add the to model + # setattr(self, attr_name, new_cls()) - def add_to_collection(self, collection_type, object_type, syntax_name, **kwargs): - raise NotImplementedError + # def add_to_collection(self, collection_type, object_type, syntax_name, **kwargs): + # raise NotImplementedError - # # Construct object - # obj=self.factory.construct(collection_type,object_type,**kwargs) - # if syntax_name=="": - # raise RuntimeError("Must supply syntax_name for nested syntax") - - # obj.set_syntax_name(syntax_name) - - # # Obtain the object for this collection type - # collection = getattr(self, collection_type.lower()) - - # # Store in collection - # collection.add(obj) - - # Some short-hands for common operations - def add_variable(self,name,variable_type="MooseVariable"): - raise NotImplementedError - #self.add_category("Variables",variable_type,name) - - def add_bc(self): - raise NotImplementedError - - def add_ic(self): - raise NotImplementedError - - def to_str(self,print_default=False): - model_str="" - for obj_type in self.moose_objects: - obj=getattr(self,obj_type) - model_str+=obj.to_str(print_default) - return model_str - - def write(self, filename): - file_handle = open(filename,'w') - file_handle.write(self.to_str()) - file_handle.close() - print("Wrote to ",filename) + # # Construct object + # obj=self.factory.construct(collection_type,object_type,**kwargs) + # if syntax_name=="": + # raise RuntimeError("Must supply syntax_name for nested syntax") + + # obj.set_syntax_name(syntax_name) + + # # Obtain the object for this collection type + # collection = getattr(self, collection_type.lower()) + + # # Store in collection + # collection.add(obj) + + # # Some short-hands for common operations + # def add_variable(self,name,variable_type="MooseVariable"): + # raise NotImplementedError + # #self.add_category("Variables",variable_type,name) + + # def add_bc(self): + # raise NotImplementedError + + # def add_ic(self): + # raise NotImplementedError + + # def to_str(self,print_default=False): + # model_str="" + # for obj_type in self.moose_objects: + # obj=getattr(self,obj_type) + # model_str+=obj.to_str(print_default) + # return model_str + + # def write(self, filename): + # file_handle = open(filename,'w') + # file_handle.write(self.to_str()) + # file_handle.close() + # print("Wrote to ",filename) diff --git a/catbird/syntax.py b/catbird/syntax.py index d2f2987..e8288e5 100644 --- a/catbird/syntax.py +++ b/catbird/syntax.py @@ -1,10 +1,10 @@ """Classes and functions to parse MOOSE syntax""" from collections.abc import Iterable from copy import deepcopy -from .cbird import Catbird -from .action import MOOSEAction -from .system import MOOSESystem -from .collection import MOOSECollection +from .cbird import MooseObject +from .action import MooseAction +from .system import MooseSystem +from .collection import MooseCollection type_mapping = {'Integer' : int, 'Boolean' : bool, @@ -24,12 +24,12 @@ } _mixin_map={ - "types": Catbird, - "actions": MOOSEAction, - "systems": MOOSESystem, - "type collections" : MOOSECollection, - "action collections": MOOSECollection, - "system collections": MOOSECollection + "types": MooseObject, + "actions": MooseAction, + "systems": MooseSystem, + "type collections" : MooseCollection, + "action collections": MooseCollection, + "system collections": MooseCollection } @@ -168,9 +168,9 @@ def to_dict(self, print_depth=3, verbose=False): "enabled": self.enabled, "params": self.has_params, } - #if verbose: - # if self.available_syntax: - # block_dict["available syntax"] = self.available_syntax + if verbose: + if self.available_syntax: + block_dict["available syntax"] = self.available_syntax # if self.parent_blocks: # block_dict["parents"] = self.parent_blocks @@ -384,9 +384,9 @@ def parse_block(json_obj,block_path): # Available syntax for this block as dict block=get_block(json_obj,block_path) - # Create new subclass of Catbird with a name that matches the block + # Create new subclass of MooseObject with a name that matches the block name=block_path.name - new_cls = type(name, (Catbird,), dict()) + new_cls = type(name, (MooseObject,), dict()) # Add parameters as attributes params=block["parameters"] diff --git a/catbird/system.py b/catbird/system.py index 65e721d..dcb6c8e 100644 --- a/catbird/system.py +++ b/catbird/system.py @@ -1,3 +1,3 @@ -class MOOSESystem(): +class MooseSystem(): def __init__(self): pass From a2aeeba712ee7ac3f1170dd17807214d95343fef Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Tue, 12 Dec 2023 17:11:37 +0000 Subject: [PATCH 30/62] Rename file cbird to obj for more consistent module naming --- catbird/__init__.py | 4 +--- catbird/collection.py | 2 +- catbird/legacy.py | 2 +- catbird/{cbird.py => obj.py} | 0 catbird/syntax.py | 2 +- 5 files changed, 4 insertions(+), 6 deletions(-) rename catbird/{cbird.py => obj.py} (100%) diff --git a/catbird/__init__.py b/catbird/__init__.py index 7ab8356..4a8314a 100644 --- a/catbird/__init__.py +++ b/catbird/__init__.py @@ -1,5 +1,3 @@ - - -from .cbird import * +from .obj import * from .model import * from .collection import * diff --git a/catbird/collection.py b/catbird/collection.py index ede3b23..e740183 100644 --- a/catbird/collection.py +++ b/catbird/collection.py @@ -1,6 +1,6 @@ from collections.abc import MutableSet from .action import MooseAction -from .cbird import MooseObject +from .obj import MooseObject from .system import MooseSystem class MooseCollection(MutableSet): diff --git a/catbird/legacy.py b/catbird/legacy.py index 1b1110b..b28ff8b 100644 --- a/catbird/legacy.py +++ b/catbird/legacy.py @@ -1,4 +1,4 @@ -from .cbird import MooseObject +from .obj import MooseObject from .syntax import type_mapping, _convert_to_type from .utils import json_from_exec, write_json diff --git a/catbird/cbird.py b/catbird/obj.py similarity index 100% rename from catbird/cbird.py rename to catbird/obj.py diff --git a/catbird/syntax.py b/catbird/syntax.py index e8288e5..907adcb 100644 --- a/catbird/syntax.py +++ b/catbird/syntax.py @@ -1,7 +1,7 @@ """Classes and functions to parse MOOSE syntax""" from collections.abc import Iterable from copy import deepcopy -from .cbird import MooseObject +from .obj import MooseObject from .action import MooseAction from .system import MooseSystem from .collection import MooseCollection From 5b8dd3cc361effa5213c934db6e0720e5e00da26 Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Tue, 12 Dec 2023 17:39:42 +0000 Subject: [PATCH 31/62] change attribute names to be more logical --- catbird/factory.py | 20 ++++++++++---------- catbird/model.py | 15 ++++++++------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/catbird/factory.py b/catbird/factory.py index 17507ee..724b944 100644 --- a/catbird/factory.py +++ b/catbird/factory.py @@ -10,16 +10,16 @@ def __init__(self,exec_path,config_file=None): self.set_defaults() # if config_file is not None: # self.load_config(config_file) - self.load_namespaces() + self.load_root_syntax() self.load_enabled_objects(json_obj) - def load_namespaces(self): + def load_root_syntax(self): # Loop over enabled root nodes - self.namespaces={} + self.root_syntax={} for block_name, block in self.available_blocks.items(): if not ( block.enabled and block.is_root ): continue - self.namespaces[block.name]=block.get_mixins() + self.root_syntax[block.name]=block.get_mixins() def load_enabled_objects(self,json_obj): self.constructors={} @@ -55,25 +55,25 @@ def __init__(self, *args, **kwargs): return __init__ - def derive_class(self,namespace,obj_types): + def derive_class(self,root_name,obj_types): # Get mixins boilerplate - mixins=self.namespaces[namespace] + mixins=self.root_syntax[root_name] # Update mixins list by comparing types for obj_type in obj_types: - class_now=self.constructors[namespace][obj_type] + class_now=self.constructors[root_name][obj_type] for i_mixin, mixin in enumerate(mixins): if issubclass(class_now,mixin): mixins[i_mixin]=class_now break # Our fancy new mixin class - new_cls = type(namespace, tuple(mixins),{"__init__":self.__get_init_method(mixins)}) + new_cls = type(root_name, tuple(mixins),{"__init__":self.__get_init_method(mixins)}) return new_cls - def construct(self,namespace,*obj_types,**kwargs): + def construct_root(self,root_name,obj_types,kwargs): # Get class - obj_class=self.derive_class(namespace, obj_types) + obj_class=self.derive_class(root_name, obj_types) obj=obj_class() # Handle keyword arguments diff --git a/catbird/model.py b/catbird/model.py index 00d3775..8faeaa0 100644 --- a/catbird/model.py +++ b/catbird/model.py @@ -12,9 +12,10 @@ def __init__(self,factory_in): # Envisage this being overridden downstream. def set_defaults(self): - self.add_object("Executioner", "Steady") - self.add_object("Problem", "FEProblem") - self.add_object("Mesh", "GeneratedMesh") + self.add_root_syntax("Executioner", "Steady") + self.add_root_syntax("Problem", "FEProblem") + self.add_root_syntax("Mesh", "GeneratedMesh") + self.add_root_syntax("Variables") #def add_category(self, category, category_type, syntax_name=""): # # Ensure this is valid syntax @@ -47,13 +48,13 @@ def set_defaults(self): # self.moose_objects[category_key]=list() # self.moose_objects[category_key].append(category_type) - def add_object(self,namespace,object_type,**kwargs): + def add_root_syntax(self,root_name,*obj_types,**kwargs): """ - Add a MOOSE object from a given namespace to the model. + Add an object corresponding to root-level MOOSE syntax """ - obj=self.factory.construct(namespace,object_type,**kwargs) + obj=self.factory.construct_root(root_name,obj_types,kwargs) # Prefer non-capitalised attributes - attr_name=namespace.lower() + attr_name=root_name.lower() setattr(self,attr_name,obj) # def add_collection(self, collection_type): From 143b63bac07281b22e1f9796c1fff0932f881d08 Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Wed, 13 Dec 2023 08:23:19 +0000 Subject: [PATCH 32/62] just one loop over available syntax in factory --- catbird/factory.py | 75 +++++++++++++++++++++++++++------------------- 1 file changed, 44 insertions(+), 31 deletions(-) diff --git a/catbird/factory.py b/catbird/factory.py index 724b944..14618c7 100644 --- a/catbird/factory.py +++ b/catbird/factory.py @@ -10,52 +10,64 @@ def __init__(self,exec_path,config_file=None): self.set_defaults() # if config_file is not None: # self.load_config(config_file) - self.load_root_syntax() self.load_enabled_objects(json_obj) - def load_root_syntax(self): - # Loop over enabled root nodes - self.root_syntax={} - for block_name, block in self.available_blocks.items(): - if not ( block.enabled and block.is_root ): - continue - self.root_syntax[block.name]=block.get_mixins() + def _load_root_syntax(self,block_name, block): + """ + Retreive a tuple of abstract classes to mix to form our root syntax node. + """ + assert block.is_root and block.enabled + self.root_syntax[block.name]=block.get_mixins() + + def _load_leaf_syntax(self,block_name, block,json_obj): + """ + Retreive a class with attributes matching the available syntax for the block. + """ + assert block.is_leaf and block.enabled + + # Convert string to SyntaxPath + syntax_path=self.registry.syntax_dict[block_name] + + # Fetch syntax for block and make a new object type + new_class=parse_block(json_obj,syntax_path) + + # Some details about the type of object + class_name=syntax_path.name + namespace=syntax_path.parent_path[-1] + if namespace not in self.constructors.keys(): + self.constructors[namespace]={} + if class_name in self.constructors[namespace].keys(): + raise RuntimeError("Duplicated class name {} in namespace {}".format(class_name,namespace)) + + # Save class constructor + self.constructors[namespace][class_name]=new_class def load_enabled_objects(self,json_obj): self.constructors={} - # Loop over enabled leaf nodes + self.root_syntax={} + + # Loop over enabled syntax blocks for block_name, block in self.available_blocks.items(): - if not ( block.enabled and block.is_leaf ): + if not block.enabled: continue - # Convert string to SyntaxPath - syntax_path=self.registry.syntax_dict[block_name] - - # Fetch syntax for block and make a new object type - new_class=parse_block(json_obj,syntax_path) - - # Some details about the type of object - class_name=syntax_path.name - namespace=syntax_path.parent_path[-1] - if namespace not in self.constructors.keys(): - self.constructors[namespace]={} - if class_name in self.constructors[namespace].keys(): - raise RuntimeError("Duplicated class name {} in namespace {}".format(class_name,namespace)) - - # Save class constructor - self.constructors[namespace][class_name]=new_class + if block.is_leaf: + self._load_leaf_syntax(block_name, block, json_obj) + elif block.is_root: + self._load_root_syntax(block_name, block) + else: + print(block_name) @staticmethod def __get_init_method(mixins): - # Define an __init__ function for the class. + # The returned init method should call each of the mix-in base class init methods in turn. def __init__(self, *args, **kwargs): - # Call the __init__ functions of all the mix-ins - for base in mixins: - base.__init__(self, *args, **kwargs) + for base in mixins: + base.__init__(self, *args, **kwargs) return __init__ - def derive_class(self,root_name,obj_types): + """Form a new mix-in class from a tuple of classes""" # Get mixins boilerplate mixins=self.root_syntax[root_name] @@ -85,6 +97,7 @@ def construct_root(self,root_name,obj_types,kwargs): return obj + def enable_syntax(self,block_name,enable_dict=None): """ Configure what MOOSE syntax to enable. From 7bf00ca3afce53e76343d62eab5f3a59b6952c71 Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Wed, 13 Dec 2023 11:34:35 +0000 Subject: [PATCH 33/62] factory now stores constructors using relation as key --- catbird/action.py | 7 +++- catbird/base.py | 9 ++++ catbird/collection.py | 27 +----------- catbird/factory.py | 42 ++++++++++++++----- catbird/model.py | 21 +++++++--- catbird/obj.py | 4 +- catbird/syntax.py | 96 ++++++++++++++++++++++++------------------- catbird/system.py | 3 -- 8 files changed, 120 insertions(+), 89 deletions(-) create mode 100644 catbird/base.py delete mode 100644 catbird/system.py diff --git a/catbird/action.py b/catbird/action.py index bf89d42..484dfb4 100644 --- a/catbird/action.py +++ b/catbird/action.py @@ -1,3 +1,8 @@ -class MooseAction(): +from .base import MooseBase + +class MooseAction(MooseBase): def __init__(self): pass + + def to_str(self,print_default=False): + pass diff --git a/catbird/base.py b/catbird/base.py new file mode 100644 index 0000000..c151983 --- /dev/null +++ b/catbird/base.py @@ -0,0 +1,9 @@ +from abc import ABC + +class MooseBase(ABC): + """The most fundamental syntactical object.""" + def __init__(self): + pass + + def to_str(self,print_default=False): + pass diff --git a/catbird/collection.py b/catbird/collection.py index e740183..ef8a890 100644 --- a/catbird/collection.py +++ b/catbird/collection.py @@ -1,7 +1,5 @@ from collections.abc import MutableSet -from .action import MooseAction -from .obj import MooseObject -from .system import MooseSystem +from .base import MooseBase class MooseCollection(MutableSet): """A collection of MOOSE objects""" @@ -20,7 +18,7 @@ def __len__(self): return len(self.objects) def _check_type(self,obj): - pass + assert issubclass(type(obj),MooseBase) def add(self,obj): # Type checking on object, raise an error if fails @@ -43,24 +41,3 @@ def to_str(self,print_default=False): collection_str+=obj.to_str(print_default) collection_str+="[]\n" return collection_str - -class MooseObjectCollection(MooseCollection): - def __init__(self): - super().__init__() - - def _check_type(self,obj): - assert issubclass(type(obj),MooseObject) - -class MooseActionCollection(MooseCollection): - def __init__(self): - super().__init__() - - def _check_type(self,obj): - assert issubclass(type(obj),MooseAction) - -class MooseSystemCollection(MooseCollection): - def __init__(self): - super().__init__() - - def _check_type(self,obj): - assert issubclass(type(obj),MooseSystem) diff --git a/catbird/factory.py b/catbird/factory.py index 14618c7..30157b3 100644 --- a/catbird/factory.py +++ b/catbird/factory.py @@ -33,14 +33,18 @@ def _load_leaf_syntax(self,block_name, block,json_obj): # Some details about the type of object class_name=syntax_path.name - namespace=syntax_path.parent_path[-1] - if namespace not in self.constructors.keys(): - self.constructors[namespace]={} - if class_name in self.constructors[namespace].keys(): - raise RuntimeError("Duplicated class name {} in namespace {}".format(class_name,namespace)) + parent_name=syntax_path.parent_path[-1] + relation=block.relation_key + if parent_name not in self.constructors.keys(): + self.constructors[parent_name]={} + if relation not in self.constructors[parent_name].keys(): + self.constructors[parent_name][relation]={} + + if class_name in self.constructors[parent_name][relation].keys(): + raise RuntimeError("Duplicated class name {} in namespace {}.{}".format(class_name,parent_name,relation)) # Save class constructor - self.constructors[namespace][class_name]=new_class + self.constructors[parent_name][relation][class_name]=new_class def load_enabled_objects(self,json_obj): self.constructors={} @@ -67,23 +71,40 @@ def __init__(self, *args, **kwargs): return __init__ def derive_class(self,root_name,obj_types): - """Form a new mix-in class from a tuple of classes""" + """ + Form a new mix-in class from a tuple of classes + + Parameters + ---------- + rootname : str + obj_types: dict + """ + # Get mixins boilerplate mixins=self.root_syntax[root_name] # Update mixins list by comparing types - for obj_type in obj_types: - class_now=self.constructors[root_name][obj_type] + for relation_type, obj_type in obj_types.items(): + # Look up type + class_now=self.constructors[root_name][relation_type][obj_type] for i_mixin, mixin in enumerate(mixins): if issubclass(class_now,mixin): mixins[i_mixin]=class_now break # Our fancy new mixin class + # TODO: define to_dict... new_cls = type(root_name, tuple(mixins),{"__init__":self.__get_init_method(mixins)}) return new_cls def construct_root(self,root_name,obj_types,kwargs): + """ + Parameters + ---------- + rootname : str + obj_types: dict + kwargs: dict + """ # Get class obj_class=self.derive_class(root_name, obj_types) obj=obj_class() @@ -91,13 +112,12 @@ def construct_root(self,root_name,obj_types,kwargs): # Handle keyword arguments for key, value in kwargs.items(): if not hasattr(obj,key): - msg="Object type {} does not have attribute {}".format(obj_type,key) + msg="Object type {} does not have attribute {}".format(root_name,key) raise RuntimeError() setattr(obj, key, value) return obj - def enable_syntax(self,block_name,enable_dict=None): """ Configure what MOOSE syntax to enable. diff --git a/catbird/model.py b/catbird/model.py index 8faeaa0..cc764a7 100644 --- a/catbird/model.py +++ b/catbird/model.py @@ -1,4 +1,6 @@ +from copy import deepcopy from .factory import Factory +from .syntax import get_relation_kwargs class MooseModel(): """Class to represent a MOOSE model""" @@ -12,9 +14,9 @@ def __init__(self,factory_in): # Envisage this being overridden downstream. def set_defaults(self): - self.add_root_syntax("Executioner", "Steady") - self.add_root_syntax("Problem", "FEProblem") - self.add_root_syntax("Mesh", "GeneratedMesh") + self.add_root_syntax("Executioner", obj_type="Steady") + self.add_root_syntax("Problem", obj_type="FEProblem") + self.add_root_syntax("Mesh", obj_type="GeneratedMesh") self.add_root_syntax("Variables") #def add_category(self, category, category_type, syntax_name=""): @@ -48,10 +50,19 @@ def set_defaults(self): # self.moose_objects[category_key]=list() # self.moose_objects[category_key].append(category_type) - def add_root_syntax(self,root_name,*obj_types,**kwargs): + def add_root_syntax(self,root_name,**kwargs_in): """ Add an object corresponding to root-level MOOSE syntax """ + # First, pop out any relation key-word args + obj_types={} + relations=get_relation_kwargs() + kwargs=deepcopy(kwargs_in) + for keyword in kwargs_in.keys(): + if keyword in relations: + obj_type = kwargs.pop(keyword) + obj_types[keyword]=obj_type + obj=self.factory.construct_root(root_name,obj_types,kwargs) # Prefer non-capitalised attributes attr_name=root_name.lower() @@ -70,7 +81,7 @@ def add_root_syntax(self,root_name,*obj_types,**kwargs): # def add_to_collection(self, collection_type, object_type, syntax_name, **kwargs): # raise NotImplementedError - + # # Construct object # obj=self.factory.construct(collection_type,object_type,**kwargs) # if syntax_name=="": diff --git a/catbird/obj.py b/catbird/obj.py index 5bdc5bb..5262c47 100644 --- a/catbird/obj.py +++ b/catbird/obj.py @@ -1,6 +1,6 @@ -from abc import ABC from copy import deepcopy import numpy as np +from .base import MooseBase class MooseParam(): """ @@ -14,7 +14,7 @@ def __init__(self): self.dim=0 self.doc="" -class MooseObject(ABC): +class MooseObject(MooseBase): """ Class to represent typed MOOSE syntax that can add type-checked properties to itself. """ diff --git a/catbird/syntax.py b/catbird/syntax.py index 907adcb..d321a0a 100644 --- a/catbird/syntax.py +++ b/catbird/syntax.py @@ -3,7 +3,6 @@ from copy import deepcopy from .obj import MooseObject from .action import MooseAction -from .system import MooseSystem from .collection import MooseCollection type_mapping = {'Integer' : int, @@ -15,23 +14,29 @@ _relation_syntax=["blocks","subblocks","actions","star","types","subblock_types"] _relation_shorthands={ - "types": "types/", - "actions":"actions/", - "systems": "subblocks/", - "type collections" : "star/subblock_types/", - "action collections": "star/actions/", - "system collections": "star/subblocks/" + "types/":"obj_type", + "actions/": "action", + "subblocks/":"system", + "star/subblock_types/":"collection_type", + "star/actions/":"collection_action", + "star/subblocks/":"nested_system", + "star/star/actions/":"nested_collection_action", + "star/star/subblock_types/":"nested_collection_type", } _mixin_map={ - "types": MooseObject, - "actions": MooseAction, - "systems": MooseSystem, - "type collections" : MooseCollection, - "action collections": MooseCollection, - "system collections": MooseCollection + "obj_type": MooseObject, + "action": MooseAction, + "system": MooseCollection, + "collection_type" : MooseCollection, + "collection_action": MooseCollection, + "nested_system": None, # The attribute should be added one layer down + "nested_collection_action": None, # Don't support this syntax + "nested_collection_type": None, # Don't support this syntax } +def get_relation_kwargs(): + return _relation_shorthands.values() class SyntaxPath(): """ @@ -44,7 +49,7 @@ def __init__(self, syntax_path_in): self.has_params=False self.is_root=False self.parent_path=None - self.parent_relation_path=None + self.parent_relation=None self.child_paths={} self.path=deepcopy(syntax_path_in) @@ -84,7 +89,14 @@ def __init__(self, syntax_path_in): if not found_parent: raise RuntimeError("Should not get here") - self.parent_relation=relation_path + + if relation_path: + _parent_relation=self._key_from_list(relation_path) + if _parent_relation not in _relation_shorthands.keys(): + print(syntax_path_in) + raise RuntimeError("unknown relation type: {}",format(_parent_relation)) + self.parent_relation=_parent_relation + self.parent_path=syntax_path else: @@ -107,7 +119,7 @@ def add_child(self, child_syntax): assert isinstance(child_syntax,SyntaxPath) # Save mapping by relation type - relation_key=self._key_from_list(child_syntax.parent_relation) + relation_key=_relation_shorthands[child_syntax.parent_relation] if relation_key not in self.child_paths.keys(): self.child_paths[relation_key]=[] self.child_paths[relation_key].append(child_syntax.unique_key) @@ -125,27 +137,13 @@ def parent_key(self): else: return None - # @property - # def has_type(self): - # return self.has_child_type("types") - - # @property - # def has_actions(self): - # return self.has_child_type("actions") - - # @property - # def has_systems(self): - # return self.has_child_type("systems") - - # use child relation to parent type - ## relation cases - # Handle these in SyntaxBlock - #['actions']-->has action - #['subblocks']-->has system - #['types']-->has type - #['star', 'actions']-->has action_collection - #['star', 'subblock_types']--> has typed_collection - #['star', 'subblocks']--> has system_collection + @property + def relation_to_parent(self): + if self.parent_relation: + relation=_relation_shorthands[self.parent_relation] + return relation + else: + return None class SyntaxBlock(): """ @@ -157,6 +155,7 @@ def __init__(self): self.has_params=False self.enabled=False self.available_syntax={} + self.relation_key=None self.parent_blocks=[] self.depth=0 @@ -186,15 +185,26 @@ def is_leaf(self): def is_root(self): return self.depth==0 - def path_to_child(self,relation_type,child_name): - path=self.path+"/"+_relation_shorthands[relation_type]+child_name + def path_to_child(self,relation_shortname,child_name): + # Invert dictionary + found=False + relation_type=None + for test_relation_type,test_shortname in _relation_shorthands.items(): + if relation_shortname == test_shortname: + found=True + relation_type=test_relation_type + + if not found: + msg="No known relation syntax maps onto shortname {}".format(relation_shortname) + raise RuntimeError(msg) + path=self.path+"/"+relation_type+child_name return path def get_mixins(self): mixin_list=[] for relation_type in self.available_syntax.keys(): mixin_now=_mixin_map[relation_type] - if mixin_now not in mixin_list: + if mixin_now is not None and mixin_now not in mixin_list: mixin_list.append(mixin_now) return mixin_list @@ -271,8 +281,8 @@ def get_children_of_type(self,syntax_key,relation_type): def get_available_syntax(self,syntax_key): available={} - for shortname,relation in _relation_shorthands.items(): - syntax_list=self.get_children_of_type(syntax_key,relation) + for relation,shortname in _relation_shorthands.items(): + syntax_list=self.get_children_of_type(syntax_key,shortname) if len(syntax_list)>0: available[shortname]=syntax_list if len(available.keys()) == 0: @@ -287,7 +297,9 @@ def make_block(self,syntax_key): block.path=syntax_key block.has_params=syntax.has_params block.available_syntax=self.get_available_syntax(syntax_key) + block.relation_key=syntax.relation_to_parent + # Set block depth and parents if not syntax.is_root: # Recurse until we find root syntax_now=syntax diff --git a/catbird/system.py b/catbird/system.py deleted file mode 100644 index dcb6c8e..0000000 --- a/catbird/system.py +++ /dev/null @@ -1,3 +0,0 @@ -class MooseSystem(): - def __init__(self): - pass From a09bf9c2eb6f27c55fa8627a720c55d82ee000cd Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Wed, 13 Dec 2023 12:14:20 +0000 Subject: [PATCH 34/62] add mechanism to save nested syntax in factory --- catbird/factory.py | 15 +++++++-------- catbird/syntax.py | 8 ++++++++ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/catbird/factory.py b/catbird/factory.py index 30157b3..8d5d69e 100644 --- a/catbird/factory.py +++ b/catbird/factory.py @@ -16,14 +16,15 @@ def _load_root_syntax(self,block_name, block): """ Retreive a tuple of abstract classes to mix to form our root syntax node. """ - assert block.is_root and block.enabled - self.root_syntax[block.name]=block.get_mixins() + assert not block.is_leaf + print("saving root",block_name,block.longname) + self.root_syntax[block.longname]=block.get_mixins() - def _load_leaf_syntax(self,block_name, block,json_obj): + def _load_leaf_syntax(self,block_name,block,json_obj): """ Retreive a class with attributes matching the available syntax for the block. """ - assert block.is_leaf and block.enabled + assert block.is_leaf # Convert string to SyntaxPath syntax_path=self.registry.syntax_dict[block_name] @@ -55,12 +56,10 @@ def load_enabled_objects(self,json_obj): if not block.enabled: continue - if block.is_leaf: + if block.is_leaf: self._load_leaf_syntax(block_name, block, json_obj) - elif block.is_root: - self._load_root_syntax(block_name, block) else: - print(block_name) + self._load_root_syntax(block_name, block) @staticmethod def __get_init_method(mixins): diff --git a/catbird/syntax.py b/catbird/syntax.py index d321a0a..46ecc9f 100644 --- a/catbird/syntax.py +++ b/catbird/syntax.py @@ -208,6 +208,14 @@ def get_mixins(self): mixin_list.append(mixin_now) return mixin_list + @property + def longname(self): + _longname="" + for parent_name in self.parent_blocks: + _longname=_longname+parent_name+"." + _longname=_longname+self.name + return _longname + # def __init__(self, _name, _syntax_type, _known_types): # self.name=_name # self.syntax_type=_syntax_type From a372d48659e7db7e471e9ccc7298ae7d795e9d54 Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Wed, 13 Dec 2023 15:04:20 +0000 Subject: [PATCH 35/62] nested syntax may now be added in model --- catbird/factory.py | 50 ++++++++++++++++++++++----------------- catbird/model.py | 58 +++++++++++++++++++++++++++++++++++----------- catbird/syntax.py | 9 +++++++ 3 files changed, 83 insertions(+), 34 deletions(-) diff --git a/catbird/factory.py b/catbird/factory.py index 8d5d69e..ca1ced9 100644 --- a/catbird/factory.py +++ b/catbird/factory.py @@ -17,7 +17,6 @@ def _load_root_syntax(self,block_name, block): Retreive a tuple of abstract classes to mix to form our root syntax node. """ assert not block.is_leaf - print("saving root",block_name,block.longname) self.root_syntax[block.longname]=block.get_mixins() def _load_leaf_syntax(self,block_name,block,json_obj): @@ -26,21 +25,24 @@ def _load_leaf_syntax(self,block_name,block,json_obj): """ assert block.is_leaf + # Some details about the type of object + relation=block.relation_key + class_name=block.name + parent_name=block.parent_longname + # Convert string to SyntaxPath syntax_path=self.registry.syntax_dict[block_name] # Fetch syntax for block and make a new object type new_class=parse_block(json_obj,syntax_path) - # Some details about the type of object - class_name=syntax_path.name - parent_name=syntax_path.parent_path[-1] - relation=block.relation_key + # Ensure dictionary initialised if parent_name not in self.constructors.keys(): self.constructors[parent_name]={} if relation not in self.constructors[parent_name].keys(): self.constructors[parent_name][relation]={} + # Don't duplicate if class_name in self.constructors[parent_name][relation].keys(): raise RuntimeError("Duplicated class name {} in namespace {}.{}".format(class_name,parent_name,relation)) @@ -78,7 +80,6 @@ def derive_class(self,root_name,obj_types): rootname : str obj_types: dict """ - # Get mixins boilerplate mixins=self.root_syntax[root_name] @@ -131,25 +132,32 @@ def enable_syntax(self,block_name,enable_dict=None): msg="Cannot enable unknown syntax {}".format(syntax_name) raise RuntimeError(msg) - # Enable top level block syntax - self.available_blocks[syntax_name].enabled=True - block_now=self.available_blocks[syntax_name] + syntax_to_enable=[syntax_name] - # Enable sub-block types - available_sub_syntax=self.registry.get_available_syntax(syntax_name) - for syntax_shortname, syntax_list in available_sub_syntax.items(): + while len(syntax_to_enable)>0: + # Get front of queue + syntax_name_now=syntax_to_enable.pop(0) - # If provided, only enable user-specified types - if enable_dict and syntax_shortname not in enable_dict.keys(): - continue + # Enable top level block syntax + self.available_blocks[syntax_name_now].enabled=True + + # Get sub-block types + block_now=self.available_blocks[syntax_name_now] + + available_sub_syntax=self.registry.get_available_syntax(syntax_name_now) + if available_sub_syntax is not None: + for relation_shortname, syntax_list in available_sub_syntax.items(): + # If enable_dict is provided, only enable user-specified types + if enable_dict and relation_shortname not in enable_dict.keys(): + continue - for syntax_item in syntax_list: - if enable_dict and syntax_item not in enable_dict[syntax_shortname]: - continue + for syntax_item in syntax_list: + if enable_dict and syntax_item not in enable_dict[relation_shortname]: + continue - # Get longname and enable - enable_key=block_now.path_to_child(syntax_shortname,syntax_item) - self.available_blocks[enable_key].enabled=True + # Get syntax lookup key and add to queue + new_syntax=block_now.path_to_child(relation_shortname,syntax_item) + syntax_to_enable.append(new_syntax) def write_config(self,filename,print_depth=3,verbose=False): diff --git a/catbird/model.py b/catbird/model.py index cc764a7..4357ecf 100644 --- a/catbird/model.py +++ b/catbird/model.py @@ -7,17 +7,18 @@ class MooseModel(): def __init__(self,factory_in): assert isinstance(factory_in,Factory) self.factory=factory_in + #self.moose_objects={} # Add attributes to this model with default assignments - self.moose_objects={} - self.set_defaults() + self.load_default_syntax() # Envisage this being overridden downstream. - def set_defaults(self): - self.add_root_syntax("Executioner", obj_type="Steady") - self.add_root_syntax("Problem", obj_type="FEProblem") - self.add_root_syntax("Mesh", obj_type="GeneratedMesh") - self.add_root_syntax("Variables") + def load_default_syntax(self): + self.add_syntax("Executioner", obj_type="Steady") + self.add_syntax("Executioner.Predictor",obj_type="AdamsPredictor") + #self.add_syntax("Problem", obj_type="FEProblem") + #self.add_syntax("Mesh", obj_type="GeneratedMesh") + #self.add_syntax("Variables") #def add_category(self, category, category_type, syntax_name=""): # # Ensure this is valid syntax @@ -50,9 +51,9 @@ def set_defaults(self): # self.moose_objects[category_key]=list() # self.moose_objects[category_key].append(category_type) - def add_root_syntax(self,root_name,**kwargs_in): + def add_syntax(self,syntax_name,**kwargs_in): """ - Add an object corresponding to root-level MOOSE syntax + Add an object corresponding to MOOSE syntax """ # First, pop out any relation key-word args obj_types={} @@ -63,10 +64,41 @@ def add_root_syntax(self,root_name,**kwargs_in): obj_type = kwargs.pop(keyword) obj_types[keyword]=obj_type - obj=self.factory.construct_root(root_name,obj_types,kwargs) - # Prefer non-capitalised attributes - attr_name=root_name.lower() - setattr(self,attr_name,obj) + # Construct the object + obj=self.factory.construct_root(syntax_name,obj_types,kwargs) + + # Add to model + self._add_to_model(syntax_name,obj) + + def _add_to_model(self,syntax_name,obj): + """Add object to the model as an attribute""" + # Obtain sequence of objects + obj_path=syntax_name.split(sep=".") + + # Attribute name (non-capitalised) + obj_name=obj_path.pop(-1) + attr_name=obj_name.lower() + + # Recurse through parent attribute objects + parent_obj=self + while len(obj_path)>0: + new_parent_name=obj_path.pop(-1) + new_parent_name=new_parent_name.lower() + if not hasattr(parent_obj,new_parent_name): + msg="Cannot construct {}:\n".format(syntax_name) + msg=msg+"Class {} has no attribute {}".format(parent_obj.__class__.__name__,new_parent_name) + raise RuntimeError(msg) + new_parent_obj=getattr(parent_obj,new_parent_name) + parent_obj=new_parent_obj + + # Avoid overwriting + if hasattr(parent_obj,attr_name): + msg="Class {} already has attribute {}".format(parent_obj.__class__.__name__,attr_name) + raise RuntimeError(msg) + + # Add + setattr(parent_obj,attr_name,obj) + # def add_collection(self, collection_type): # # E.g. Variables, Kernels, BCs, Materials diff --git a/catbird/syntax.py b/catbird/syntax.py index 46ecc9f..82d16c8 100644 --- a/catbird/syntax.py +++ b/catbird/syntax.py @@ -208,6 +208,15 @@ def get_mixins(self): mixin_list.append(mixin_now) return mixin_list + + @property + def parent_longname(self): + _longname="" + for parent_name in self.parent_blocks: + _longname=_longname+parent_name+"." + _longname=_longname.rstrip(".") + return _longname + @property def longname(self): _longname="" From fdd48b4b634d460e0c7ef7b750cb95fb09a5043d Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Wed, 13 Dec 2023 16:13:24 +0000 Subject: [PATCH 36/62] Object + action mixins working --- catbird/action.py | 5 +++-- catbird/factory.py | 50 +++++++++++++++++++++++++++++++++++----------- catbird/model.py | 9 +++++++-- catbird/syntax.py | 34 +++++++++++++++---------------- 4 files changed, 64 insertions(+), 34 deletions(-) diff --git a/catbird/action.py b/catbird/action.py index 484dfb4..1a0e05d 100644 --- a/catbird/action.py +++ b/catbird/action.py @@ -1,7 +1,8 @@ -from .base import MooseBase +from .obj import MooseObject -class MooseAction(MooseBase): +class MooseAction(MooseObject): def __init__(self): + print("I am an action") pass def to_str(self,print_default=False): diff --git a/catbird/factory.py b/catbird/factory.py index ca1ced9..e0de936 100644 --- a/catbird/factory.py +++ b/catbird/factory.py @@ -4,13 +4,25 @@ class Factory(): """Class to contain constructors for MOOSE syntax objects""" def __init__(self,exec_path,config_file=None): + print("Loading syntax from library...") json_obj=json_from_exec(exec_path) + print("Done.") + + print("Constructing syntax registry...") self.registry=SyntaxRegistry(json_obj) + print("Done.") + self.available_blocks=self.registry.get_available_blocks() + + print("Configuring objects to enable...") self.set_defaults() + print("Done.") + # if config_file is not None: # self.load_config(config_file) + print("Loading enabled objects...") self.load_enabled_objects(json_obj) + print("Done.") def _load_root_syntax(self,block_name, block): """ @@ -48,6 +60,7 @@ def _load_leaf_syntax(self,block_name,block,json_obj): # Save class constructor self.constructors[parent_name][relation][class_name]=new_class + print("New constructor constructors[{}][{}][{}]".format(parent_name,relation,class_name)) def load_enabled_objects(self,json_obj): self.constructors={} @@ -84,17 +97,24 @@ def derive_class(self,root_name,obj_types): mixins=self.root_syntax[root_name] # Update mixins list by comparing types - for relation_type, obj_type in obj_types.items(): - # Look up type - class_now=self.constructors[root_name][relation_type][obj_type] - for i_mixin, mixin in enumerate(mixins): - if issubclass(class_now,mixin): - mixins[i_mixin]=class_now - break + mixin_list=[] + for relation_type,derived_type in obj_types.items(): + if relation_type not in mixins.keys(): + raise RuntimeError("{} is not an available mix-in".format(relation_type)) + mixin_base_class=mixins[relation_type] + + # Fetch derived class for mix-in + class_now=self.constructors[root_name][relation_type][derived_type] + + # Check provided object type is of the correct type + if not issubclass(class_now,mixin_base_class): + raise RuntimeError("{} has wrong base class".format(derived_type)) + + mixin_list.append(class_now) # Our fancy new mixin class # TODO: define to_dict... - new_cls = type(root_name, tuple(mixins),{"__init__":self.__get_init_method(mixins)}) + new_cls = type(root_name, tuple(mixin_list),{"__init__":self.__get_init_method(mixins)}) return new_cls def construct_root(self,root_name,obj_types,kwargs): @@ -175,7 +195,13 @@ def load_config(self,filename): self.available_blocks[block_name].enabled=block_dict["enabled"] def set_defaults(self): - self.enable_syntax("Mesh") - self.enable_syntax("Executioner") - self.enable_syntax("Problem") - self.enable_syntax("Variables") + #self.enable_syntax("Mesh") + mesh_enable_dict={ + "action": ["CreateDisplacedProblemAction", + "DisplayGhostingAction"], + "obj_type": ["FileMesh","GeneratedMesh"] + } + self.enable_syntax("Mesh",enable_dict=mesh_enable_dict) + #self.enable_syntax("Executioner") + #self.enable_syntax("Problem") + #self.enable_syntax("Variables") diff --git a/catbird/model.py b/catbird/model.py index 4357ecf..4eebe80 100644 --- a/catbird/model.py +++ b/catbird/model.py @@ -14,10 +14,15 @@ def __init__(self,factory_in): # Envisage this being overridden downstream. def load_default_syntax(self): - self.add_syntax("Executioner", obj_type="Steady") - self.add_syntax("Executioner.Predictor",obj_type="AdamsPredictor") + #self.add_syntax("Executioner", obj_type="Steady") + #self.add_syntax("Executioner.Predictor",obj_type="AdamsPredictor") #self.add_syntax("Problem", obj_type="FEProblem") #self.add_syntax("Mesh", obj_type="GeneratedMesh") + + self.add_syntax("Mesh", + obj_type="GeneratedMesh", + action="CreateDisplacedProblemAction") + #self.add_syntax("Variables") #def add_category(self, category, category_type, syntax_name=""): diff --git a/catbird/syntax.py b/catbird/syntax.py index 82d16c8..aeabd48 100644 --- a/catbird/syntax.py +++ b/catbird/syntax.py @@ -27,9 +27,9 @@ _mixin_map={ "obj_type": MooseObject, "action": MooseAction, - "system": MooseCollection, + "system": None, "collection_type" : MooseCollection, - "collection_action": MooseCollection, + "collection_action": None, "nested_system": None, # The attribute should be added one layer down "nested_collection_action": None, # Don't support this syntax "nested_collection_type": None, # Don't support this syntax @@ -201,12 +201,11 @@ def path_to_child(self,relation_shortname,child_name): return path def get_mixins(self): - mixin_list=[] + mixin_dict={} for relation_type in self.available_syntax.keys(): mixin_now=_mixin_map[relation_type] - if mixin_now is not None and mixin_now not in mixin_list: - mixin_list.append(mixin_now) - return mixin_list + mixin_dict[relation_type]=mixin_now + return mixin_dict @property @@ -386,8 +385,9 @@ def key_search_recurse(dict_in, test_path, key_test, level_stop=15): return success_paths -def get_block(json_dict,syntax): +def fetch_syntax(json_dict,syntax): assert isinstance(syntax,SyntaxPath) + assert syntax.has_params key_list=deepcopy(syntax.path) assert len(key_list) > 0 @@ -400,22 +400,20 @@ def get_block(json_dict,syntax): assert isinstance(obj_now,dict) dict_now=deepcopy(obj_now) - try: - assert syntax.has_params - except AssertionError: - print(syntax.name) - print(dict_now.keys()) - raise AssertionError - return dict_now -def parse_block(json_obj,block_path): +def parse_block(json_obj,syntax_path): + print("Parsing block",syntax_path.unique_key) # Available syntax for this block as dict - block=get_block(json_obj,block_path) + block=fetch_syntax(json_obj,syntax_path) # Create new subclass of MooseObject with a name that matches the block - name=block_path.name - new_cls = type(name, (MooseObject,), dict()) + name=syntax_path.name + + # Deduce type of object + relation=_relation_shorthands[syntax_path.parent_relation] + class_type=_mixin_map[relation] + new_cls = type(name, (class_type,), dict()) # Add parameters as attributes params=block["parameters"] From 4bcd943cf763ec6976522ef4a5adca118a43a5f3 Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Thu, 14 Dec 2023 09:57:59 +0000 Subject: [PATCH 37/62] keep track of object and action params seperately --- catbird/action.py | 10 ++- catbird/base.py | 134 +++++++++++++++++++++++++++++++++- catbird/obj.py | 179 +++------------------------------------------- 3 files changed, 143 insertions(+), 180 deletions(-) diff --git a/catbird/action.py b/catbird/action.py index 1a0e05d..2d43a39 100644 --- a/catbird/action.py +++ b/catbird/action.py @@ -1,9 +1,7 @@ -from .obj import MooseObject +from .base import MooseBase + +class MooseAction(MooseBase): + _moose_action_params=[] -class MooseAction(MooseObject): def __init__(self): print("I am an action") - pass - - def to_str(self,print_default=False): - pass diff --git a/catbird/base.py b/catbird/base.py index c151983..874841d 100644 --- a/catbird/base.py +++ b/catbird/base.py @@ -1,9 +1,137 @@ from abc import ABC +class MooseParam(): + """ + Class to contain all information about a MOOSE parameter + """ + def __init__(self): + self.val=None + self.attr_type=None + self.default=None + self.allowed_vals=None + self.dim=0 + self.doc="" + class MooseBase(ABC): - """The most fundamental syntactical object.""" + """ + Class that can add type-checked properties to itself. + """ def __init__(self): pass - def to_str(self,print_default=False): - pass + @staticmethod + def check_type(name, val, attr_type): + """Checks a value's type""" + if not isinstance(val, attr_type): + val_type_str = val.__class__.__name__ + exp_type_str = attr_type.__name__ + raise ValueError(f'Incorrect type "{val_type_str}" for attribute "{name}". ' + f'Expected type "{exp_type_str}".') + return val + + @staticmethod + def check_vals(name, val, allowed_vals): + """Checks that a value is in the set of allowed_values""" + if val not in allowed_vals: + raise ValueError(f'Value {val} for attribute {name} is not one of {allowed_vals}') + + @staticmethod + def moose_property(name, param): + """ + Returns a property, and creates an associated class attribute whose value is that of the supplied MooseParam. + + The property setter method will change the value of the underlying MooseParam.value, checking its type is consistent + The property getter method will retrieve the value of the underlying MooseParam.value + """ + + def fget(self): + # set to the default value if the internal attribute doesn't exist + if not hasattr(self, '_'+name): + setattr(self, '_'+name, param) + param_now = getattr(self, '_'+name) + return param_now.val + + def fset(self, val): + if param.dim == 0: + self.check_type(name, val, param.attr_type) + if param.allowed_vals is not None: + self.check_vals(name, val, param.allowed_vals) + else: + val = np.asarray(val) + self.check_type(name, val.flat[0].item(), param.attr_type) + if len(val.shape) != param.dim: + raise ValueError(f'Dimensionality is incorrect. Expects a {dim}-D array.') + for v in val.flatten(): + if param.allowed_vals is not None: + self.check_vals(name, v, allowed_vals) + + param_now = getattr(self, '_'+name) + param_now.val=val + setattr(self, '_'+name, param_now) + + def fdel(self): + param_now = getattr(self, '_'+name) + del param_now + + + return property(fget,fset,fdel,param.doc) + + @classmethod + def newattr(cls, attr_name, attr_type=str, dim=0, default=None, allowed_vals=None, description=None): + """Adds a property to the class""" + if not isinstance(attr_name, str): + raise ValueError('Attributes must be strings') + + if attr_name.find("_syntax_") != -1: + msg="'_syntax_' is reserved attribute string. Cannot add attibute {}".format(attr_name) + raise RuntimeError(msg) + + # Set attribute docstring + doc_str = f'\nType: {attr_type.__name__}\n' + if description is not None: + doc_str += description + if allowed_vals is not None: + doc_str += f'\nValues: {allowed_vals}' + + # Store parameter details in a structure + moose_param=MooseParam() + if default is not None: + moose_param.val=default + else: + moose_param.val=attr_type() + moose_param.attr_type=attr_type + moose_param.default=default + moose_param.dim=dim + moose_param.allowed_vals=allowed_vals + moose_param.doc=doc_str + + # Add attribute to the class using a method which returns a property + setattr(cls, attr_name, cls.moose_property(attr_name,moose_param)) + + # Keep track of the attributes we've added + # Cheeky coding to avoid downstream clashes with mixins + params_name=None + if hasattr(cls,"_moose_params"): + params_name="_moose_params" + elif hasattr(cls,"_moose_action_params"): + params_name="_moose_action_params" + + if params_name: + moose_param_list_local=getattr(cls,params_name) + moose_param_list_local.append(attr_name) + setattr(cls,params_name,moose_param_list_local) + + + @property + def moose_params(self): + """ + Return a unified list of all the parameters we've added. + """ + moose_param_list_local=[] + if hasattr(self,"_moose_params"): + moose_param_list_local.extend(getattr(self,"_moose_params")) + + if hasattr(self,"_moose_action_params"): + moose_param_list_local.extend(getattr(self,"_moose_action_params")) + + return moose_param_list_local diff --git a/catbird/obj.py b/catbird/obj.py index 5262c47..da3adc9 100644 --- a/catbird/obj.py +++ b/catbird/obj.py @@ -1,28 +1,13 @@ -from copy import deepcopy -import numpy as np from .base import MooseBase -class MooseParam(): - """ - Class to contain all information about a MOOSE parameter - """ - def __init__(self): - self.val=None - self.attr_type=None - self.default=None - self.allowed_vals=None - self.dim=0 - self.doc="" - class MooseObject(MooseBase): - """ - Class to represent typed MOOSE syntax that can add type-checked properties to itself. - """ + _moose_params=[] + def __init__(self): - self._syntax_name="" + print("I am an object") - def set_syntax_name(self,syntax_name): - self._syntax_name=syntax_name + # def set_syntax_name(self,syntax_name): + # self._syntax_name=syntax_name # @classmethod # def set_syntax_type(cls,syntax_type): @@ -50,156 +35,9 @@ def set_syntax_name(self,syntax_name): # def is_nested(self): # return self._syntax_type =="nested" or self._syntax_type=="nested_system" - @property - def moose_params(self): - return self._moose_params - - @staticmethod - def check_type(name, val, attr_type): - """Checks a value's type""" - if not isinstance(val, attr_type): - val_type_str = val.__class__.__name__ - exp_type_str = attr_type.__name__ - raise ValueError(f'Incorrect type "{val_type_str}" for attribute "{name}". ' - f'Expected type "{exp_type_str}".') - return val - - @staticmethod - def check_vals(name, val, allowed_vals): - """Checks that a value is in the set of allowed_values""" - if val not in allowed_vals: - raise ValueError(f'Value {val} for attribute {name} is not one of {allowed_vals}') - - @staticmethod - def moose_property(name, param): - """ - Returns a property, and creates an associated class attribute whose value is that of the supplied MooseParam. - - The property setter method will change the value of the underlying MooseParam.value, checking its type is consistent - The property getter method will retrieve the value of the underlying MooseParam.value - """ - - def fget(self): - # set to the default value if the internal attribute doesn't exist - if not hasattr(self, '_'+name): - setattr(self, '_'+name, param) - param_now = getattr(self, '_'+name) - return param_now.val - - def fset(self, val): - if param.dim == 0: - self.check_type(name, val, param.attr_type) - if param.allowed_vals is not None: - self.check_vals(name, val, param.allowed_vals) - else: - val = np.asarray(val) - self.check_type(name, val.flat[0].item(), param.attr_type) - if len(val.shape) != param.dim: - raise ValueError(f'Dimensionality is incorrect. Expects a {dim}-D array.') - for v in val.flatten(): - if param.allowed_vals is not None: - self.check_vals(name, v, allowed_vals) - - param_now = getattr(self, '_'+name) - param_now.val=val - setattr(self, '_'+name, param_now) - - def fdel(self): - param_now = getattr(self, '_'+name) - del param_now - - - return property(fget,fset,fdel,param.doc) - - # @staticmethod - # def prop_get(name, default=None): - # """Returns function for getting an attribute""" - # def fget(self): - # # set to the default value if the internal attribute doesn't exist - # if not hasattr(self, '_'+name): - # setattr(self, '_'+name, default) - # value = getattr(self, '_'+name) - # return value - # return fget - - # @staticmethod - # def prop_set(name, attr_type, dim=0, allowed_vals=None,doc=""): - # """Returns a function for setting an attribute""" - # def fset(self, val): - # if dim == 0: - # self.check_type(name, val, attr_type) - # if allowed_vals is not None: - # self.check_vals(name, val, allowed_vals) - # setattr(self, '_'+name, val) - # #setattr(self, '_'+name, val) - # else: - # val = np.asarray(val) - # self.check_type(name, val.flat[0].item(), attr_type) - # if len(val.shape) != dim: - # raise ValueError(f'Dimensionality is incorrect. Expects a {dim}-D array.') - # for v in val.flatten(): - # if allowed_vals is not None: - # self.check_vals(name, v, allowed_vals) - # setattr(self, '_'+name, val) - # return fset - - # @staticmethod - # def prop_doc(name, doc_str=""): - # def fdoc(self): - # print("retrieve doc") - # doc_str_now="" - # if doc_str != "": - # doc_str_now=doc_str - # else: - # value = getattr(self, '_'+name) - # doc_str_now=value.__doc__ - # return doc_str_now - # return fdoc - - @classmethod - def newattr(cls, attr_name, attr_type=str, dim=0, default=None, allowed_vals=None, description=None): - """Adds a property to the class""" - if not isinstance(attr_name, str): - raise ValueError('Attributes must be strings') - - if attr_name.find("_syntax_") != -1: - msg="'_syntax_' is reserved attribute string. Cannot add attibute {}".format(attr_name) - raise RuntimeError(msg) - - # Set attribute docstring - doc_str = f'\nType: {attr_type.__name__}\n' - if description is not None: - doc_str += description - if allowed_vals is not None: - doc_str += f'\nValues: {allowed_vals}' - - # Store parameter details in a structure - moose_param=MooseParam() - if default is not None: - moose_param.val=default - else: - moose_param.val=attr_type() - moose_param.attr_type=attr_type - moose_param.default=default - moose_param.dim=dim - moose_param.allowed_vals=allowed_vals - moose_param.doc=doc_str - - # Define a property and add to class (args are functions) - # Should be able to add a docstring here.... - #prop = property(fget=cls.prop_get(attr_name, default), - # fset=cls.prop_set(attr_name, attr_type, dim, allowed_vals, doc_str)) - #setattr(cls, attr_name, prop) - - # Add attribute to the class using a method which returns a property - setattr(cls, attr_name, cls.moose_property(attr_name,moose_param)) - - # Keep track of the attributes we've added - if not hasattr(cls,"_moose_params"): - setattr(cls,"_moose_params",[]) - moose_param_list_local=getattr(cls,"_moose_params") - moose_param_list_local.append(attr_name) - setattr(cls,"_moose_params",moose_param_list_local) + # @property + # def moose_params(self): + # return self._moose_params # # Todo - waspify # def to_node(self): @@ -282,4 +120,3 @@ def print_me(self): else: attr_str="{}.{}: None".format(name,attr_name) print(attr_str) - From db7a74da6c9c2da7c76f0542979710c2b3f82313 Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Thu, 14 Dec 2023 14:52:23 +0000 Subject: [PATCH 38/62] to_str method is working again for non-nested --- catbird/action.py | 3 +- catbird/base.py | 85 ++++++++++++++++++++++++++++---- catbird/factory.py | 3 +- catbird/model.py | 4 +- catbird/obj.py | 118 +-------------------------------------------- 5 files changed, 84 insertions(+), 129 deletions(-) diff --git a/catbird/action.py b/catbird/action.py index 2d43a39..1e70a1c 100644 --- a/catbird/action.py +++ b/catbird/action.py @@ -1,7 +1,8 @@ from .base import MooseBase class MooseAction(MooseBase): - _moose_action_params=[] + + params_name="_moose_action_params" def __init__(self): print("I am an action") diff --git a/catbird/base.py b/catbird/base.py index 874841d..34f5e7d 100644 --- a/catbird/base.py +++ b/catbird/base.py @@ -109,17 +109,13 @@ def newattr(cls, attr_name, attr_type=str, dim=0, default=None, allowed_vals=Non setattr(cls, attr_name, cls.moose_property(attr_name,moose_param)) # Keep track of the attributes we've added - # Cheeky coding to avoid downstream clashes with mixins params_name=None - if hasattr(cls,"_moose_params"): - params_name="_moose_params" - elif hasattr(cls,"_moose_action_params"): - params_name="_moose_action_params" - - if params_name: - moose_param_list_local=getattr(cls,params_name) + if cls.params_name: + if not hasattr(cls,cls.params_name): + setattr(cls,cls.params_name,[]) + moose_param_list_local=getattr(cls,cls.params_name) moose_param_list_local.append(attr_name) - setattr(cls,params_name,moose_param_list_local) + setattr(cls,cls.params_name,moose_param_list_local) @property @@ -135,3 +131,74 @@ def moose_params(self): moose_param_list_local.extend(getattr(self,"_moose_action_params")) return moose_param_list_local + + @property + def print_name(self): + """ + Return name for printing purposes + """ + class_name=self.__class__.__name__ + class_path=class_name.split(sep=".") + print_name=class_path[-1] + return print_name + + @property + def indent_level(self): + """ + Return level of indent for printing purposes + """ + class_name=self.__class__.__name__ + class_path=class_name.split(sep=".") + indent_level=len(class_path) + return indent_level + + @property + def indent(self): + indent_str="" + for i_level in range(self.indent_level): + # Use two space indent + indent_str=indent_str+" " + return indent_str + + @property + def prepend_indent(self): + indent_str="" + for i_level in range(self.indent_level-1): + # Use two space indent + indent_str=indent_str+" " + return indent_str + + def is_default(self,attr_name): + attr_val = getattr(self, attr_name) + param = getattr(self, "_"+attr_name) + default_val = param.default + if default_val is None: + default_val = param.attr_type() + return attr_val == default_val + + def attr_to_str(self,attr_name,print_default=False): + attr_str="" + if self.is_default(attr_name) and not print_default: + return attr_str + + attr_val = getattr(self, attr_name) + if attr_val is not None: + attr_str=self.indent+'{}={}\n'.format(attr_name,attr_val) + return attr_str + + + def to_str(self,print_default=False): + syntax_str='{}[{}]\n'.format(self.prepend_indent,self.print_name) + + param_list=self.moose_params + + # Formatting convention, start with type + if "type" in param_list: + param_list.remove("type") + syntax_str+=self.attr_to_str("type",True) + + for attr_name in param_list: + syntax_str+=self.attr_to_str(attr_name,print_default) + syntax_str+='{}[]\n'.format(self.prepend_indent) + + return syntax_str diff --git a/catbird/factory.py b/catbird/factory.py index e0de936..5cf9996 100644 --- a/catbird/factory.py +++ b/catbird/factory.py @@ -115,6 +115,7 @@ def derive_class(self,root_name,obj_types): # Our fancy new mixin class # TODO: define to_dict... new_cls = type(root_name, tuple(mixin_list),{"__init__":self.__get_init_method(mixins)}) + return new_cls def construct_root(self,root_name,obj_types,kwargs): @@ -202,6 +203,6 @@ def set_defaults(self): "obj_type": ["FileMesh","GeneratedMesh"] } self.enable_syntax("Mesh",enable_dict=mesh_enable_dict) - #self.enable_syntax("Executioner") + self.enable_syntax("Executioner") #self.enable_syntax("Problem") #self.enable_syntax("Variables") diff --git a/catbird/model.py b/catbird/model.py index 4eebe80..a0c9528 100644 --- a/catbird/model.py +++ b/catbird/model.py @@ -14,8 +14,8 @@ def __init__(self,factory_in): # Envisage this being overridden downstream. def load_default_syntax(self): - #self.add_syntax("Executioner", obj_type="Steady") - #self.add_syntax("Executioner.Predictor",obj_type="AdamsPredictor") + self.add_syntax("Executioner", obj_type="Steady") + self.add_syntax("Executioner.Predictor",obj_type="AdamsPredictor") #self.add_syntax("Problem", obj_type="FEProblem") #self.add_syntax("Mesh", obj_type="GeneratedMesh") diff --git a/catbird/obj.py b/catbird/obj.py index da3adc9..1770917 100644 --- a/catbird/obj.py +++ b/catbird/obj.py @@ -1,122 +1,8 @@ from .base import MooseBase class MooseObject(MooseBase): - _moose_params=[] + + params_name="_moose_params" def __init__(self): print("I am an object") - - # def set_syntax_name(self,syntax_name): - # self._syntax_name=syntax_name - - # @classmethod - # def set_syntax_type(cls,syntax_type): - # cls._syntax_type=syntax_type - - # @classmethod - # def set_syntax_category(cls,syntax_category): - # cls._syntax_category=syntax_category - - # @property - # def syntax_block_name(self): - # if self.is_nested: - # return self._syntax_name - # else: - # return self._syntax_category - - # @property - # def indent_level(self): - # if self.is_nested: - # return 2 - # else: - # return 1 - - # @property - # def is_nested(self): - # return self._syntax_type =="nested" or self._syntax_type=="nested_system" - - # @property - # def moose_params(self): - # return self._moose_params - - # # Todo - waspify - # def to_node(self): - # """ - # Create a pyhit node for this MOOSE object - # """ - # import pyhit - - # node = pyhit.Node(hitnode=self.__class__.__name__) - - # for attr in self.__moose_attrs__: - # val = getattr(self, attr) - - # getattr(self, '_'+name) - # if val is not None: - # node[attr] = val - - # return node - - @property - def indent(self): - indent_str="" - indent_per_level=" " - for i_level in range(0,self.indent_level): - indent_str+=indent_per_level - return indent_str - - @property - def prepend_indent(self): - indent_str="" - indent_per_level=" " - if self.indent_level > 1: - for i_level in range(0,self.indent_level-1): - indent_str+=indent_per_level - return indent_str - - def is_default(self,attr_name): - attr_val = getattr(self, attr_name) - param = getattr(self, "_"+attr_name) - default_val = param.default - if default_val is None: - default_val = param.attr_type() - return attr_val == default_val - - def attr_to_str(self,attr_name,print_default=False): - attr_str="" - if self.is_default(attr_name) and not print_default: - return attr_str - - attr_val = getattr(self, attr_name) - if attr_val is not None: - attr_val = getattr(self, attr_name) - attr_str=self.indent+'{}={}\n'.format(attr_name,attr_val) - return attr_str - - def to_str(self,print_default=False): - syntax_str='{}[{}]\n'.format(self.prepend_indent,self.syntax_block_name) - param_list=self.moose_params - - # Formatting convention, start with type - if "type" in param_list: - param_list.remove("type") - syntax_str+=self.attr_to_str("type",True) - - for attr_name in param_list: - syntax_str+=self.attr_to_str(attr_name,print_default) - syntax_str+='{}[]\n'.format(self.prepend_indent) - - return syntax_str - - def print_me(self): - name=self.block_name - print("Name: ",name) - - param_list=self.moose_params - for attr_name in param_list: - attr_val = getattr(self, attr_name) - if attr_val is not None: - attr_str="{}.{}: {}".format(name,attr_name,attr_val) - else: - attr_str="{}.{}: None".format(name,attr_name) - print(attr_str) From 35bb7e5ad5e3065bcd08fca2e460cda9a9e08c24 Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Thu, 14 Dec 2023 17:36:32 +0000 Subject: [PATCH 39/62] printing works for collections and mixins --- catbird/action.py | 25 +++++++++++++-- catbird/base.py | 71 ++----------------------------------------- catbird/collection.py | 22 +++++++------- catbird/factory.py | 34 ++++++++++++++++++--- catbird/model.py | 17 +++++++---- catbird/obj.py | 26 ++++++++++++++-- catbird/string.py | 70 ++++++++++++++++++++++++++++++++++++++++++ catbird/syntax.py | 30 ++++++++++++------ 8 files changed, 191 insertions(+), 104 deletions(-) create mode 100644 catbird/string.py diff --git a/catbird/action.py b/catbird/action.py index 1e70a1c..03f073b 100644 --- a/catbird/action.py +++ b/catbird/action.py @@ -1,8 +1,29 @@ from .base import MooseBase class MooseAction(MooseBase): - params_name="_moose_action_params" def __init__(self): - print("I am an action") + pass + + @property + def moose_action_params(self): + """ + Return a unified list of all the parameters we've added. + """ + moose_param_list_local=[] + if hasattr(self,"_moose_action_params"): + moose_param_list_local=getattr(self,"_moose_action_params") + return moose_param_list_local + + def inner_to_str(self,print_default=False): + inner_str="" + param_list=self.moose_action_params + + # We don't print the type of actions + if "type" in param_list: + param_list.remove("type") + + for attr_name in param_list: + inner_str+=self.attr_to_str(attr_name,print_default) + return inner_str diff --git a/catbird/base.py b/catbird/base.py index 34f5e7d..feb2dcb 100644 --- a/catbird/base.py +++ b/catbird/base.py @@ -1,4 +1,5 @@ from abc import ABC +from .string import MooseString class MooseParam(): """ @@ -12,7 +13,7 @@ def __init__(self): self.dim=0 self.doc="" -class MooseBase(ABC): +class MooseBase(ABC,MooseString): """ Class that can add type-checked properties to itself. """ @@ -117,57 +118,6 @@ def newattr(cls, attr_name, attr_type=str, dim=0, default=None, allowed_vals=Non moose_param_list_local.append(attr_name) setattr(cls,cls.params_name,moose_param_list_local) - - @property - def moose_params(self): - """ - Return a unified list of all the parameters we've added. - """ - moose_param_list_local=[] - if hasattr(self,"_moose_params"): - moose_param_list_local.extend(getattr(self,"_moose_params")) - - if hasattr(self,"_moose_action_params"): - moose_param_list_local.extend(getattr(self,"_moose_action_params")) - - return moose_param_list_local - - @property - def print_name(self): - """ - Return name for printing purposes - """ - class_name=self.__class__.__name__ - class_path=class_name.split(sep=".") - print_name=class_path[-1] - return print_name - - @property - def indent_level(self): - """ - Return level of indent for printing purposes - """ - class_name=self.__class__.__name__ - class_path=class_name.split(sep=".") - indent_level=len(class_path) - return indent_level - - @property - def indent(self): - indent_str="" - for i_level in range(self.indent_level): - # Use two space indent - indent_str=indent_str+" " - return indent_str - - @property - def prepend_indent(self): - indent_str="" - for i_level in range(self.indent_level-1): - # Use two space indent - indent_str=indent_str+" " - return indent_str - def is_default(self,attr_name): attr_val = getattr(self, attr_name) param = getattr(self, "_"+attr_name) @@ -185,20 +135,3 @@ def attr_to_str(self,attr_name,print_default=False): if attr_val is not None: attr_str=self.indent+'{}={}\n'.format(attr_name,attr_val) return attr_str - - - def to_str(self,print_default=False): - syntax_str='{}[{}]\n'.format(self.prepend_indent,self.print_name) - - param_list=self.moose_params - - # Formatting convention, start with type - if "type" in param_list: - param_list.remove("type") - syntax_str+=self.attr_to_str("type",True) - - for attr_name in param_list: - syntax_str+=self.attr_to_str(attr_name,print_default) - syntax_str+='{}[]\n'.format(self.prepend_indent) - - return syntax_str diff --git a/catbird/collection.py b/catbird/collection.py index ef8a890..455adb9 100644 --- a/catbird/collection.py +++ b/catbird/collection.py @@ -1,11 +1,13 @@ from collections.abc import MutableSet from .base import MooseBase +from .action import MooseAction +from .obj import MooseObject +from .string import MooseString -class MooseCollection(MutableSet): +class MooseCollection(MutableSet,MooseString): """A collection of MOOSE objects""" def __init__(self): self.objects={} - self._type=None # Define mandatory methods def __contains__(self,key): @@ -20,24 +22,22 @@ def __len__(self): def _check_type(self,obj): assert issubclass(type(obj),MooseBase) - def add(self,obj): + def add(self,obj,lookup_name): # Type checking on object, raise an error if fails self._check_type(obj) - block_name=obj.syntax_block_name - if block_name in self.objects.keys(): + if lookup_name in self.objects.keys(): msg="Collection already contains named block {}".format(block_name) raise RuntimeError(msg) # Index - self.objects[block_name]=obj + self.objects[lookup_name]=obj def discard(self,key): self.objects.pop(key) - def to_str(self,print_default=False): - collection_str="[{}]\n".format(self.__class__.__name__) + def inner_to_str(self,print_default=False): + inner_str="" for name, obj in self.objects.items(): - collection_str+=obj.to_str(print_default) - collection_str+="[]\n" - return collection_str + inner_str+=obj.to_str(print_default) + return inner_str diff --git a/catbird/factory.py b/catbird/factory.py index 5cf9996..6bab275 100644 --- a/catbird/factory.py +++ b/catbird/factory.py @@ -1,3 +1,4 @@ +from copy import deepcopy from .syntax import SyntaxRegistry, parse_block from .utils import read_json, write_json, json_from_exec @@ -84,6 +85,17 @@ def __init__(self, *args, **kwargs): base.__init__(self, *args, **kwargs) return __init__ + @staticmethod + def __get_inner_to_str_method(mixins): + # The returned _inner_to_str_ method should call each of the mix-in base class methods in turn and concatenate. + def inner_to_str(self,print_default=False): + inner_str="" + for base in mixins: + inner_str+=base.inner_to_str(self) + return inner_str + return inner_to_str + + def derive_class(self,root_name,obj_types): """ Form a new mix-in class from a tuple of classes @@ -97,7 +109,7 @@ def derive_class(self,root_name,obj_types): mixins=self.root_syntax[root_name] # Update mixins list by comparing types - mixin_list=[] + mixins_now=deepcopy(mixins) for relation_type,derived_type in obj_types.items(): if relation_type not in mixins.keys(): raise RuntimeError("{} is not an available mix-in".format(relation_type)) @@ -110,12 +122,24 @@ def derive_class(self,root_name,obj_types): if not issubclass(class_now,mixin_base_class): raise RuntimeError("{} has wrong base class".format(derived_type)) - mixin_list.append(class_now) + # Update + mixins_now[relation_type]=class_now - # Our fancy new mixin class - # TODO: define to_dict... - new_cls = type(root_name, tuple(mixin_list),{"__init__":self.__get_init_method(mixins)}) + # Finally, remove duplicates but preserve order + mixin_list=[] + for mixin_test in mixins_now.values(): + if mixin_test not in mixin_list: + mixin_list.append(mixin_test) + # Convert to tuple + mixin_tuple=tuple(mixin_list) + + # Our fancy new mixin class + new_cls = type(root_name, mixin_tuple, + { + "__init__":self.__get_init_method(mixin_tuple), + "inner_to_str":self.__get_inner_to_str_method(mixin_tuple), + }) return new_cls def construct_root(self,root_name,obj_types,kwargs): diff --git a/catbird/model.py b/catbird/model.py index a0c9528..6066513 100644 --- a/catbird/model.py +++ b/catbird/model.py @@ -1,7 +1,9 @@ from copy import deepcopy +from .collection import MooseCollection from .factory import Factory from .syntax import get_relation_kwargs +# TODO should this also be a MooseCollection? class MooseModel(): """Class to represent a MOOSE model""" def __init__(self,factory_in): @@ -18,11 +20,9 @@ def load_default_syntax(self): self.add_syntax("Executioner.Predictor",obj_type="AdamsPredictor") #self.add_syntax("Problem", obj_type="FEProblem") #self.add_syntax("Mesh", obj_type="GeneratedMesh") - self.add_syntax("Mesh", obj_type="GeneratedMesh", action="CreateDisplacedProblemAction") - #self.add_syntax("Variables") #def add_category(self, category, category_type, syntax_name=""): @@ -73,12 +73,13 @@ def add_syntax(self,syntax_name,**kwargs_in): obj=self.factory.construct_root(syntax_name,obj_types,kwargs) # Add to model - self._add_to_model(syntax_name,obj) + self._add_to_model(obj) - def _add_to_model(self,syntax_name,obj): + def _add_to_model(self,obj): """Add object to the model as an attribute""" # Obtain sequence of objects - obj_path=syntax_name.split(sep=".") + obj_classname=obj.__class__.__name__ + obj_path=obj_classname.split(sep=".") # Attribute name (non-capitalised) obj_name=obj_path.pop(-1) @@ -101,9 +102,13 @@ def _add_to_model(self,syntax_name,obj): msg="Class {} already has attribute {}".format(parent_obj.__class__.__name__,attr_name) raise RuntimeError(msg) - # Add + # Add as attribute setattr(parent_obj,attr_name,obj) + # If the parent is a collection, add there for book-keeping purposes + # N.B. this is to support subblock syntax + if isinstance(parent_obj,MooseCollection): + parent_obj.add(obj,attr_name) # def add_collection(self, collection_type): # # E.g. Variables, Kernels, BCs, Materials diff --git a/catbird/obj.py b/catbird/obj.py index 1770917..6abe639 100644 --- a/catbird/obj.py +++ b/catbird/obj.py @@ -1,8 +1,30 @@ from .base import MooseBase class MooseObject(MooseBase): - params_name="_moose_params" def __init__(self): - print("I am an object") + pass + + @property + def moose_object_params(self): + """ + Return a unified list of all the parameters we've added. + """ + moose_param_list_local=[] + if hasattr(self,"_moose_params"): + moose_param_list_local=getattr(self,"_moose_params") + return moose_param_list_local + + def inner_to_str(self,print_default=False): + inner_str="" + param_list=self.moose_object_params + + # Formatting convention, start with type + if "type" in param_list: + param_list.remove("type") + inner_str+=self.attr_to_str("type",True) + + for attr_name in param_list: + inner_str+=self.attr_to_str(attr_name,print_default) + return inner_str diff --git a/catbird/string.py b/catbird/string.py new file mode 100644 index 0000000..7a9bbc1 --- /dev/null +++ b/catbird/string.py @@ -0,0 +1,70 @@ +class MooseString(): + """Mixin to assist printing""" + def set_lookup_name(self,name_in): + self._lookup_name=name_in + + @property + def lookup_name(self): + """Optional property for externally set name""" + if hasattr(self,"_lookup_name"): + return self._lookup_name + else: + return None + + @property + def print_name(self): + """ + Return name for printing purposes + """ + if self.lookup_name: + return self.lookup_name + + class_name=self.__class__.__name__ + class_path=class_name.split(sep=".") + print_name=class_path[-1] + return print_name + + @property + def indent_level(self): + """ + Return level of indent for printing purposes + """ + class_name=self.__class__.__name__ + class_path=class_name.split(sep=".") + indent_level=len(class_path) + return indent_level + + @property + def indent(self): + """ + Inner indent string + """ + indent_str="" + for i_level in range(self.indent_level): + # Use two space indent + indent_str=indent_str+" " + return indent_str + + @property + def prepend_indent(self): + """ + Outer indent string + """ + indent_str="" + for i_level in range(self.indent_level-1): + # Use two space indent + indent_str=indent_str+" " + return indent_str + + def to_str(self,print_default=False): + """ + Return syntax as a string + """ + syntax_str='{}[{}]\n'.format(self.prepend_indent,self.print_name) + syntax_str+=self.inner_to_str(print_default) + syntax_str+='{}[]\n'.format(self.prepend_indent) + return syntax_str + + def inner_to_str(self,print_default=False): + # Override me + pass diff --git a/catbird/syntax.py b/catbird/syntax.py index aeabd48..0a27084 100644 --- a/catbird/syntax.py +++ b/catbird/syntax.py @@ -3,7 +3,7 @@ from copy import deepcopy from .obj import MooseObject from .action import MooseAction -from .collection import MooseCollection +from .collection import * type_mapping = {'Integer' : int, 'Boolean' : bool, @@ -27,14 +27,26 @@ _mixin_map={ "obj_type": MooseObject, "action": MooseAction, - "system": None, + "system": MooseCollection, "collection_type" : MooseCollection, - "collection_action": None, + "collection_action": MooseCollection, # Don't support this syntax yet + "nested_system": None, # The attribute should be added one layer down + "nested_collection_action": None, # Don't support this syntax yet + "nested_collection_type": None, # Don't support this syntax yet +} + +_child_type_map={ + "obj_type": MooseObject, + "action": MooseAction, + "system": None, + "collection_type" : MooseObject, + "collection_action": MooseAction, # Don't support this syntax yet "nested_system": None, # The attribute should be added one layer down - "nested_collection_action": None, # Don't support this syntax - "nested_collection_type": None, # Don't support this syntax + "nested_collection_action": None, # Don't support this syntax yet + "nested_collection_type": None, # Don't support this syntax yet } + def get_relation_kwargs(): return _relation_shorthands.values() @@ -204,7 +216,8 @@ def get_mixins(self): mixin_dict={} for relation_type in self.available_syntax.keys(): mixin_now=_mixin_map[relation_type] - mixin_dict[relation_type]=mixin_now + if mixin_now is not None: + mixin_dict[relation_type]=mixin_now return mixin_dict @@ -403,16 +416,15 @@ def fetch_syntax(json_dict,syntax): return dict_now def parse_block(json_obj,syntax_path): - print("Parsing block",syntax_path.unique_key) # Available syntax for this block as dict block=fetch_syntax(json_obj,syntax_path) # Create new subclass of MooseObject with a name that matches the block name=syntax_path.name - # Deduce type of object + # Deduce type of object by its relation to parent relation=_relation_shorthands[syntax_path.parent_relation] - class_type=_mixin_map[relation] + class_type=_child_type_map[relation] new_cls = type(name, (class_type,), dict()) # Add parameters as attributes From 5fe999a73546ad9af34a8d8871ee8c76b241c433 Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Tue, 19 Dec 2023 16:48:58 +0000 Subject: [PATCH 40/62] Adding objects to collections works again --- catbird/collection.py | 6 ++++- catbird/factory.py | 28 +++++++++++++++++------ catbird/model.py | 53 +++++++++++++++++++++++++++---------------- catbird/syntax.py | 4 ++-- 4 files changed, 62 insertions(+), 29 deletions(-) diff --git a/catbird/collection.py b/catbird/collection.py index 455adb9..9c17314 100644 --- a/catbird/collection.py +++ b/catbird/collection.py @@ -26,11 +26,15 @@ def add(self,obj,lookup_name): # Type checking on object, raise an error if fails self._check_type(obj) + # Set the name of the object + obj.set_lookup_name(lookup_name) + + # Don't dupicate entries in collection if lookup_name in self.objects.keys(): msg="Collection already contains named block {}".format(block_name) raise RuntimeError(msg) - # Index + # Save self.objects[lookup_name]=obj def discard(self,key): diff --git a/catbird/factory.py b/catbird/factory.py index 6bab275..80c7448 100644 --- a/catbird/factory.py +++ b/catbird/factory.py @@ -40,14 +40,15 @@ def _load_leaf_syntax(self,block_name,block,json_obj): # Some details about the type of object relation=block.relation_key - class_name=block.name + lookup_name=block.name + class_name=block.longname parent_name=block.parent_longname # Convert string to SyntaxPath syntax_path=self.registry.syntax_dict[block_name] # Fetch syntax for block and make a new object type - new_class=parse_block(json_obj,syntax_path) + new_class=parse_block(json_obj,syntax_path,class_name) # Ensure dictionary initialised if parent_name not in self.constructors.keys(): @@ -60,8 +61,8 @@ def _load_leaf_syntax(self,block_name,block,json_obj): raise RuntimeError("Duplicated class name {} in namespace {}.{}".format(class_name,parent_name,relation)) # Save class constructor - self.constructors[parent_name][relation][class_name]=new_class - print("New constructor constructors[{}][{}][{}]".format(parent_name,relation,class_name)) + self.constructors[parent_name][relation][lookup_name]=new_class + print("New constructor constructors[{}][{}][{}]={}".format(parent_name,relation,lookup_name,class_name)) def load_enabled_objects(self,json_obj): self.constructors={} @@ -163,6 +164,20 @@ def construct_root(self,root_name,obj_types,kwargs): return obj + def construct(self,root_name,relation_type,derived_type, **kwargs): + print(root_name,relation_type,derived_type) + + class_now=self.constructors[root_name][relation_type][derived_type] + obj=class_now() + # Handle keyword arguments + for key, value in kwargs.items(): + print(key,value) + if not hasattr(obj,key): + msg="Object type {} does not have attribute {}".format(root_name,key) + raise RuntimeError() + setattr(obj, key, value) + return obj + def enable_syntax(self,block_name,enable_dict=None): """ Configure what MOOSE syntax to enable. @@ -204,7 +219,6 @@ def enable_syntax(self,block_name,enable_dict=None): new_syntax=block_now.path_to_child(relation_shortname,syntax_item) syntax_to_enable.append(new_syntax) - def write_config(self,filename,print_depth=3,verbose=False): config_dict={} for block_name, block in self.available_blocks.items(): @@ -226,7 +240,7 @@ def set_defaults(self): "DisplayGhostingAction"], "obj_type": ["FileMesh","GeneratedMesh"] } - self.enable_syntax("Mesh",enable_dict=mesh_enable_dict) + #self.enable_syntax("Mesh",enable_dict=mesh_enable_dict) self.enable_syntax("Executioner") #self.enable_syntax("Problem") - #self.enable_syntax("Variables") + self.enable_syntax("Variables") diff --git a/catbird/model.py b/catbird/model.py index 6066513..9b488af 100644 --- a/catbird/model.py +++ b/catbird/model.py @@ -20,10 +20,10 @@ def load_default_syntax(self): self.add_syntax("Executioner.Predictor",obj_type="AdamsPredictor") #self.add_syntax("Problem", obj_type="FEProblem") #self.add_syntax("Mesh", obj_type="GeneratedMesh") - self.add_syntax("Mesh", - obj_type="GeneratedMesh", - action="CreateDisplacedProblemAction") - #self.add_syntax("Variables") + #self.add_syntax("Mesh", + # obj_type="GeneratedMesh", + # action="CreateDisplacedProblemAction") + self.add_syntax("Variables") #def add_category(self, category, category_type, syntax_name=""): # # Ensure this is valid syntax @@ -121,27 +121,42 @@ def _add_to_model(self,obj): # # Construct and add the to model # setattr(self, attr_name, new_cls()) - # def add_to_collection(self, collection_type, object_type, syntax_name, **kwargs): - # raise NotImplementedError + def add_to_collection(self, collection_name, object_name,**kwargs_in): + # First, pop out any relation key-word args + obj_type_kwargs={} + relations=get_relation_kwargs() + kwargs=deepcopy(kwargs_in) + for keyword in kwargs_in.keys(): + if keyword in relations: + obj_type_value = kwargs.pop(keyword) + obj_type_kwargs[keyword]=obj_type_value + print(keyword,obj_type_value) - # # Construct object - # obj=self.factory.construct(collection_type,object_type,**kwargs) - # if syntax_name=="": - # raise RuntimeError("Must supply syntax_name for nested syntax") + relations=list(obj_type_kwargs.keys()) + obj_classes=list(obj_type_kwargs.values()) - # obj.set_syntax_name(syntax_name) + # If more than one type, error! + if len(relations) > 1: + msg="Cannot add mixin types to collection" + raise RuntimeError(msg) - # # Obtain the object for this collection type - # collection = getattr(self, collection_type.lower()) + # One basic type is mandatory + if len(relations) == 0: + msg="Must specify a relation type" + raise RuntimeError(msg) - # # Store in collection - # collection.add(obj) + relation=relations[0] + obj_class_name=obj_classes[0] - # # Some short-hands for common operations - # def add_variable(self,name,variable_type="MooseVariable"): - # raise NotImplementedError - # #self.add_category("Variables",variable_type,name) + obj=self.factory.construct(collection_name,relation,obj_class_name,**kwargs) + + # Fetch collection and add + collection = getattr(self, collection_name.lower()) + collection.add(obj,object_name) + # Some short-hands for common operations + def add_variable(self,variable_name,variable_type="MooseVariable"): + self.add_to_collection("Variables",variable_name,collection_type=variable_type,order="SECOND") # def add_bc(self): # raise NotImplementedError diff --git a/catbird/syntax.py b/catbird/syntax.py index 0a27084..062231c 100644 --- a/catbird/syntax.py +++ b/catbird/syntax.py @@ -415,12 +415,12 @@ def fetch_syntax(json_dict,syntax): return dict_now -def parse_block(json_obj,syntax_path): +def parse_block(json_obj,syntax_path,class_name): # Available syntax for this block as dict block=fetch_syntax(json_obj,syntax_path) # Create new subclass of MooseObject with a name that matches the block - name=syntax_path.name + name=class_name # Deduce type of object by its relation to parent relation=_relation_shorthands[syntax_path.parent_relation] From aa8190fa5709f139efe9959215154759a9b30bcb Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Tue, 19 Dec 2023 17:10:14 +0000 Subject: [PATCH 41/62] some tidying, restore model functionality --- catbird/collection.py | 2 +- catbird/factory.py | 4 +-- catbird/model.py | 68 ++++++++++--------------------------------- 3 files changed, 18 insertions(+), 56 deletions(-) diff --git a/catbird/collection.py b/catbird/collection.py index 9c17314..790f005 100644 --- a/catbird/collection.py +++ b/catbird/collection.py @@ -22,7 +22,7 @@ def __len__(self): def _check_type(self,obj): assert issubclass(type(obj),MooseBase) - def add(self,obj,lookup_name): + def add(self,obj,lookup_name,as_attribute=False): # Type checking on object, raise an error if fails self._check_type(obj) diff --git a/catbird/factory.py b/catbird/factory.py index 80c7448..37b6932 100644 --- a/catbird/factory.py +++ b/catbird/factory.py @@ -240,7 +240,7 @@ def set_defaults(self): "DisplayGhostingAction"], "obj_type": ["FileMesh","GeneratedMesh"] } - #self.enable_syntax("Mesh",enable_dict=mesh_enable_dict) + self.enable_syntax("Mesh",enable_dict=mesh_enable_dict) self.enable_syntax("Executioner") - #self.enable_syntax("Problem") + self.enable_syntax("Problem") self.enable_syntax("Variables") diff --git a/catbird/model.py b/catbird/model.py index 9b488af..5d28de9 100644 --- a/catbird/model.py +++ b/catbird/model.py @@ -9,7 +9,7 @@ class MooseModel(): def __init__(self,factory_in): assert isinstance(factory_in,Factory) self.factory=factory_in - #self.moose_objects={} + self.moose_objects=[] # Add attributes to this model with default assignments self.load_default_syntax() @@ -18,44 +18,12 @@ def __init__(self,factory_in): def load_default_syntax(self): self.add_syntax("Executioner", obj_type="Steady") self.add_syntax("Executioner.Predictor",obj_type="AdamsPredictor") - #self.add_syntax("Problem", obj_type="FEProblem") - #self.add_syntax("Mesh", obj_type="GeneratedMesh") - #self.add_syntax("Mesh", - # obj_type="GeneratedMesh", - # action="CreateDisplacedProblemAction") + self.add_syntax("Problem", obj_type="FEProblem") + self.add_syntax("Mesh", + obj_type="GeneratedMesh", + action="CreateDisplacedProblemAction") self.add_syntax("Variables") - #def add_category(self, category, category_type, syntax_name=""): - # # Ensure this is valid syntax - # if category not in self.factory.constructors.keys(): - # msg="Invalid block type {}".format(category) - # raise RuntimeError(msg) - - # # First look up the syntax type - # syntax_type=self.factory.available_blocks[category].syntax_type - - # # How to add depends on syntax type - # if syntax_type == "fundamental": - # # If fundmantal, just add. We're done. - # self.add_object(category,category_type) - # elif syntax_type == "nested": - # if not hasattr(self,category): - # self.add_collection(category) - # self.add_to_collection(category,category_type,syntax_name) - # elif syntax_type == "system": - # raise NotImplementedError() - # elif syntax_type == "nested_system": - # raise NotImplementedError() - # else: - # msg="Unhandled syntax type {}".format(syntax_type) - # raise RuntimeError(msg) - - # # Object has been constructed, now just book-keeping - # category_key=category.lower() - # if category_key not in self.moose_objects.keys(): - # self.moose_objects[category_key]=list() - # self.moose_objects[category_key].append(category_type) - def add_syntax(self,syntax_name,**kwargs_in): """ Add an object corresponding to MOOSE syntax @@ -110,16 +78,9 @@ def _add_to_model(self,obj): if isinstance(parent_obj,MooseCollection): parent_obj.add(obj,attr_name) - # def add_collection(self, collection_type): - # # E.g. Variables, Kernels, BCs, Materials - # # Create new subclass of with a name that matches the collection_type - # new_cls = type(collection_type, (MOOSECollection,), dict()) - - # # Prefer non-capitalised attributes - # attr_name=collection_type.lower() - - # # Construct and add the to model - # setattr(self, attr_name, new_cls()) + # Book-keeping + if parent_obj == self: + self.moose_objects.append(attr_name) def add_to_collection(self, collection_name, object_name,**kwargs_in): # First, pop out any relation key-word args @@ -157,18 +118,19 @@ def add_to_collection(self, collection_name, object_name,**kwargs_in): # Some short-hands for common operations def add_variable(self,variable_name,variable_type="MooseVariable"): self.add_to_collection("Variables",variable_name,collection_type=variable_type,order="SECOND") + # def add_bc(self): # raise NotImplementedError # def add_ic(self): # raise NotImplementedError - # def to_str(self,print_default=False): - # model_str="" - # for obj_type in self.moose_objects: - # obj=getattr(self,obj_type) - # model_str+=obj.to_str(print_default) - # return model_str + def to_str(self,print_default=False): + model_str="" + for obj_type in self.moose_objects: + obj=getattr(self,obj_type) + model_str+=obj.to_str(print_default) + return model_str # def write(self, filename): # file_handle = open(filename,'w') From a57d3c59b1c60d20043d09d420692d0c7ad6ab74 Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Tue, 19 Dec 2023 17:25:30 +0000 Subject: [PATCH 42/62] restore capability to write model to file; add kwargs to variables --- catbird/factory.py | 5 ++--- catbird/model.py | 23 +++++++++-------------- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/catbird/factory.py b/catbird/factory.py index 37b6932..3aa03fd 100644 --- a/catbird/factory.py +++ b/catbird/factory.py @@ -62,7 +62,7 @@ def _load_leaf_syntax(self,block_name,block,json_obj): # Save class constructor self.constructors[parent_name][relation][lookup_name]=new_class - print("New constructor constructors[{}][{}][{}]={}".format(parent_name,relation,lookup_name,class_name)) + def load_enabled_objects(self,json_obj): self.constructors={} @@ -166,12 +166,11 @@ def construct_root(self,root_name,obj_types,kwargs): def construct(self,root_name,relation_type,derived_type, **kwargs): print(root_name,relation_type,derived_type) - + class_now=self.constructors[root_name][relation_type][derived_type] obj=class_now() # Handle keyword arguments for key, value in kwargs.items(): - print(key,value) if not hasattr(obj,key): msg="Object type {} does not have attribute {}".format(root_name,key) raise RuntimeError() diff --git a/catbird/model.py b/catbird/model.py index 5d28de9..5b6f573 100644 --- a/catbird/model.py +++ b/catbird/model.py @@ -91,7 +91,6 @@ def add_to_collection(self, collection_name, object_name,**kwargs_in): if keyword in relations: obj_type_value = kwargs.pop(keyword) obj_type_kwargs[keyword]=obj_type_value - print(keyword,obj_type_value) relations=list(obj_type_kwargs.keys()) obj_classes=list(obj_type_kwargs.values()) @@ -116,14 +115,10 @@ def add_to_collection(self, collection_name, object_name,**kwargs_in): collection.add(obj,object_name) # Some short-hands for common operations - def add_variable(self,variable_name,variable_type="MooseVariable"): - self.add_to_collection("Variables",variable_name,collection_type=variable_type,order="SECOND") - - # def add_bc(self): - # raise NotImplementedError - - # def add_ic(self): - # raise NotImplementedError + def add_variable(self,variable_name,variable_type="MooseVariable",**kwargs_in): + collection_kwargs=deepcopy(kwargs_in) + collection_kwargs["collection_type"]=variable_type + self.add_to_collection("Variables",variable_name,**collection_kwargs) def to_str(self,print_default=False): model_str="" @@ -132,8 +127,8 @@ def to_str(self,print_default=False): model_str+=obj.to_str(print_default) return model_str - # def write(self, filename): - # file_handle = open(filename,'w') - # file_handle.write(self.to_str()) - # file_handle.close() - # print("Wrote to ",filename) + def write(self, filename): + file_handle = open(filename,'w') + file_handle.write(self.to_str()) + file_handle.close() + print("Wrote to ",filename) From 0d3fc4eb4d1cf25d250b8ccced53faba0e1319dc Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Tue, 19 Dec 2023 17:28:15 +0000 Subject: [PATCH 43/62] undo accidental change to collection.add --- catbird/collection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/catbird/collection.py b/catbird/collection.py index 790f005..9c17314 100644 --- a/catbird/collection.py +++ b/catbird/collection.py @@ -22,7 +22,7 @@ def __len__(self): def _check_type(self,obj): assert issubclass(type(obj),MooseBase) - def add(self,obj,lookup_name,as_attribute=False): + def add(self,obj,lookup_name): # Type checking on object, raise an error if fails self._check_type(obj) From 1772207a1e92194841a4d719c651bf21a8355b11 Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Wed, 20 Dec 2023 08:58:24 +0000 Subject: [PATCH 44/62] missing error message --- catbird/factory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/catbird/factory.py b/catbird/factory.py index 3aa03fd..0932297 100644 --- a/catbird/factory.py +++ b/catbird/factory.py @@ -173,7 +173,7 @@ def construct(self,root_name,relation_type,derived_type, **kwargs): for key, value in kwargs.items(): if not hasattr(obj,key): msg="Object type {} does not have attribute {}".format(root_name,key) - raise RuntimeError() + raise RuntimeError(msg) setattr(obj, key, value) return obj From 1a622ed9e47e4bc508b9fae4cf1fb560a4075c7a Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Wed, 20 Dec 2023 17:20:03 +0000 Subject: [PATCH 45/62] WIP. Attempt to prevent static class creation --- catbird/action.py | 5 +- catbird/base.py | 192 +++++++++++++++++++++++++++++++++------------ catbird/factory.py | 13 ++- catbird/obj.py | 6 +- catbird/string.py | 4 + catbird/syntax.py | 5 +- 6 files changed, 170 insertions(+), 55 deletions(-) diff --git a/catbird/action.py b/catbird/action.py index 03f073b..57b08b2 100644 --- a/catbird/action.py +++ b/catbird/action.py @@ -4,7 +4,7 @@ class MooseAction(MooseBase): params_name="_moose_action_params" def __init__(self): - pass + super().__init__() @property def moose_action_params(self): @@ -13,7 +13,8 @@ def moose_action_params(self): """ moose_param_list_local=[] if hasattr(self,"_moose_action_params"): - moose_param_list_local=getattr(self,"_moose_action_params") + moose_param_dict_local=getattr(self,"_moose_action_params") + moose_param_list_local=list(moose_param_dict_local.keys()) return moose_param_list_local def inner_to_str(self,print_default=False): diff --git a/catbird/base.py b/catbird/base.py index feb2dcb..2168dcd 100644 --- a/catbird/base.py +++ b/catbird/base.py @@ -13,12 +13,93 @@ def __init__(self): self.dim=0 self.doc="" +# class MyDescriptor: +# def __init__(self, value, attr_type, dim): +# self._value = value +# self._attr_type=attr_type +# self._dim=dim + +# def __get__(self, owner_instance, owner_type): +# print("We're getting from MyDescriptor") +# return owner_instance._value + +# def __set__(self, owner_instance, value): +# print("We're setting from MyDescriptor") +# value_to_set=value +# if isinstance(value,MyDescriptor): +# value_to_set=value._value + +# if self._.dim == 0: +# self.check_type(value_to_set,self._attr_type) +# # if param.allowed_vals is not None: +# # obj.check_vals(name, val, param.allowed_vals) +# else: +# val = np.asarray(value_to_set) +# self.check_type(val.flat[0].item(), self._attr_type) +# if len(val.shape) != param.dim: +# raise ValueError(f'Dimensionality is incorrect. Expects a {dim}-D array.') +# # for v in val.flatten(): +# # if param.allowed_vals is not None: +# # obj.check_vals(name, v, allowed_vals) +# self._value = value_to_set + +# @staticmethod +# def check_type(val, attr_type): +# """Checks a value's type""" +# if not isinstance(val, attr_type): +# val_type_str = val.__class__.__name__ +# exp_type_str = attr_type.__name__ +# raise ValueError(f'Incorrect type "{val_type_str}".' +# f'Expected type "{exp_type_str}".') + +# @staticmethod +# def check_vals(val, allowed_vals): +# """Checks that a value is in the set of allowed_values""" +# if val not in allowed_vals: +# raise ValueError(f'Value {val} is not one of {allowed_vals}') + class MooseBase(ABC,MooseString): """ Class that can add type-checked properties to itself. """ + def __setattr__(self, attr_name, value): + if hasattr(self,attr_name): + type_now=type(getattr(self,attr_name)) + if not isinstance(value,type_now): + msg="Attribute {} should have type {}".format(attr_name,type_now) + raise ValueError(msg) + super().__setattr__(attr_name, value) + + # def __getattribute__(self, attr): + # print("We're here") + # # If the attribute does not exist, super().__getattribute__() + # # will raise an AttributeError + # got_attr = super().__getattribute__(attr) + # try: + # # Try "manually" invoking the descriptor protocol __get__() + # return got_attr.__get__(self, type(self)) + # except AttributeError: + # # Attribute is not a descriptor, just return it: + # return got_attr + + def __init__(self): - pass + print("In the MooseBase init method of ",self.__class__.__name__) + + # List of the attributes this class should have + moose_param_dict_local=getattr(self,self.params_name) + + # Loop over and make into properties + for attr_name, moose_param in moose_param_dict_local.items(): + # Crucially, acts on the instance, not the class. + setattr(self,attr_name,moose_param.val) + + # print(" Adding property: ",attr_name) + + # # Property object (hopefully acts on OBJ) + # new_prop=self.moose_property(self,attr_name,moose_param) + # self.add_as_property(self,attr_name,new_prop) + @staticmethod def check_type(name, val, attr_type): @@ -36,46 +117,47 @@ def check_vals(name, val, allowed_vals): if val not in allowed_vals: raise ValueError(f'Value {val} for attribute {name} is not one of {allowed_vals}') - @staticmethod - def moose_property(name, param): - """ - Returns a property, and creates an associated class attribute whose value is that of the supplied MooseParam. - - The property setter method will change the value of the underlying MooseParam.value, checking its type is consistent - The property getter method will retrieve the value of the underlying MooseParam.value - """ - - def fget(self): - # set to the default value if the internal attribute doesn't exist - if not hasattr(self, '_'+name): - setattr(self, '_'+name, param) - param_now = getattr(self, '_'+name) - return param_now.val - - def fset(self, val): - if param.dim == 0: - self.check_type(name, val, param.attr_type) - if param.allowed_vals is not None: - self.check_vals(name, val, param.allowed_vals) - else: - val = np.asarray(val) - self.check_type(name, val.flat[0].item(), param.attr_type) - if len(val.shape) != param.dim: - raise ValueError(f'Dimensionality is incorrect. Expects a {dim}-D array.') - for v in val.flatten(): - if param.allowed_vals is not None: - self.check_vals(name, v, allowed_vals) - - param_now = getattr(self, '_'+name) - param_now.val=val - setattr(self, '_'+name, param_now) - - def fdel(self): - param_now = getattr(self, '_'+name) - del param_now - - - return property(fget,fset,fdel,param.doc) + # @staticmethod + # def moose_property(obj,name,param): + # """ + # Returns a property, and creates an associated class attribute whose value is that of the supplied MooseParam. + + # The property setter method will change the value of the underlying MooseParam.value, checking its type is consistent + # The property getter method will retrieve the value of the underlying MooseParam.value + # """ + + # def fget(self): + # # set to the default value if the internal attribute doesn't exist + # if not hasattr(obj, '_'+name): + # setattr(obj, '_'+name, param) + # param_now = getattr(obj, '_'+name) + # return param_now.val + + # def fset(self, val): + # print("setting", obj.lookup_name) + # if param.dim == 0: + # obj.check_type(name, val, param.attr_type) + # if param.allowed_vals is not None: + # obj.check_vals(name, val, param.allowed_vals) + # else: + # val = np.asarray(val) + # obj.check_type(name, val.flat[0].item(), param.attr_type) + # if len(val.shape) != param.dim: + # raise ValueError(f'Dimensionality is incorrect. Expects a {dim}-D array.') + # for v in val.flatten(): + # if param.allowed_vals is not None: + # obj.check_vals(name, v, allowed_vals) + + # param_now = getattr(obj, '_'+name) + # param_now.val=val + # setattr(obj, '_'+name, param_now) + + # def fdel(self): + # param_now = getattr(obj, '_'+name) + # del param_now + + + # return property(fget,None,fdel,param.doc) @classmethod def newattr(cls, attr_name, attr_type=str, dim=0, default=None, allowed_vals=None, description=None): @@ -106,17 +188,19 @@ def newattr(cls, attr_name, attr_type=str, dim=0, default=None, allowed_vals=Non moose_param.allowed_vals=allowed_vals moose_param.doc=doc_str - # Add attribute to the class using a method which returns a property - setattr(cls, attr_name, cls.moose_property(attr_name,moose_param)) - - # Keep track of the attributes we've added + # Store attribute in dict params_name=None if cls.params_name: if not hasattr(cls,cls.params_name): - setattr(cls,cls.params_name,[]) - moose_param_list_local=getattr(cls,cls.params_name) - moose_param_list_local.append(attr_name) - setattr(cls,cls.params_name,moose_param_list_local) + setattr(cls,cls.params_name,{}) + moose_param_dict_local=getattr(cls,cls.params_name) + moose_param_dict_local[attr_name]=moose_param + setattr(cls,cls.params_name,moose_param_dict_local) + + + @staticmethod + def add_as_property(obj,attr_name,property_obj): + setattr(obj, attr_name, property_obj) def is_default(self,attr_name): attr_val = getattr(self, attr_name) @@ -135,3 +219,13 @@ def attr_to_str(self,attr_name,print_default=False): if attr_val is not None: attr_str=self.indent+'{}={}\n'.format(attr_name,attr_val) return attr_str + + def moose_doc(self): + doc_now="Custom MooseBase docstring\n" + # moose_param_dict_local={} + # if hasattr(self,"_moose_params"): + # moose_param_dict_local=getattr(self,"_moose_params") + # for attr_name, param in moose_param_dict_local.items(): + # doc_now=doc_now+param.doc + # docs_now=super().__doc__+doc_now + return doc_now diff --git a/catbird/factory.py b/catbird/factory.py index 0932297..fd89c56 100644 --- a/catbird/factory.py +++ b/catbird/factory.py @@ -96,6 +96,12 @@ def inner_to_str(self,print_default=False): return inner_str return inner_to_str + @staticmethod + def __get_docstring(mixins): + doc_now="Fa la la la la la la la la\n" + for base in mixins: + doc_now=doc_now+base.moose_doc(base) + return doc_now def derive_class(self,root_name,obj_types): """ @@ -135,10 +141,15 @@ def derive_class(self,root_name,obj_types): # Convert to tuple mixin_tuple=tuple(mixin_list) + + #doc_str_now=self.__get_docstring(mixin_tuple) + #print(doc_str_now) + # Our fancy new mixin class new_cls = type(root_name, mixin_tuple, { - "__init__":self.__get_init_method(mixin_tuple), + "__init__": self.__get_init_method(mixin_tuple), + "__doc__": self.__get_docstring(mixin_tuple), "inner_to_str":self.__get_inner_to_str_method(mixin_tuple), }) return new_cls diff --git a/catbird/obj.py b/catbird/obj.py index 6abe639..0a498cf 100644 --- a/catbird/obj.py +++ b/catbird/obj.py @@ -1,10 +1,11 @@ from .base import MooseBase class MooseObject(MooseBase): + """Tralalala""" params_name="_moose_params" def __init__(self): - pass + super().__init__() @property def moose_object_params(self): @@ -13,7 +14,8 @@ def moose_object_params(self): """ moose_param_list_local=[] if hasattr(self,"_moose_params"): - moose_param_list_local=getattr(self,"_moose_params") + moose_param_dict_local=getattr(self,"_moose_params") + moose_param_list_local=list(moose_param_dict_local.keys()) return moose_param_list_local def inner_to_str(self,print_default=False): diff --git a/catbird/string.py b/catbird/string.py index 7a9bbc1..796b120 100644 --- a/catbird/string.py +++ b/catbird/string.py @@ -68,3 +68,7 @@ def to_str(self,print_default=False): def inner_to_str(self,print_default=False): # Override me pass + + def moose_doc(self): + # Override me + return "" diff --git a/catbird/syntax.py b/catbird/syntax.py index 062231c..41083b5 100644 --- a/catbird/syntax.py +++ b/catbird/syntax.py @@ -425,7 +425,10 @@ def parse_block(json_obj,syntax_path,class_name): # Deduce type of object by its relation to parent relation=_relation_shorthands[syntax_path.parent_relation] class_type=_child_type_map[relation] - new_cls = type(name, (class_type,), dict()) + + #print(class_type().moose_doc) + new_cls = type(name, + (class_type,),dict()) # Add parameters as attributes params=block["parameters"] From 51070df24690e799b5d9a43157e97147f49c589a Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Wed, 3 Jan 2024 13:12:31 +0000 Subject: [PATCH 46/62] Documentation fixed --- catbird/action.py | 22 ++++++++++++++++++++++ catbird/base.py | 23 ++++++++++------------- catbird/factory.py | 3 ++- catbird/obj.py | 23 ++++++++++++++++++++++- 4 files changed, 56 insertions(+), 15 deletions(-) diff --git a/catbird/action.py b/catbird/action.py index 57b08b2..a652892 100644 --- a/catbird/action.py +++ b/catbird/action.py @@ -28,3 +28,25 @@ def inner_to_str(self,print_default=False): for attr_name in param_list: inner_str+=self.attr_to_str(attr_name,print_default) return inner_str + + + def moose_doc(self): + doc_now='' + + # Documentation for all the parameters + moose_param_dict_local={} + if hasattr(self,"_moose_action_params"): + moose_param_dict_local=getattr(self,"_moose_action_params") + + # We don't print the type of actions + if "type" in moose_param_dict_local.keys(): + param=moose_param_dict_local.pop("type") + + if len(moose_param_dict_local.keys())>0: + doc_now='MOOSE Action Parameters\n' + doc_now+='-----------------------n' + + for attr_name, param in moose_param_dict_local.items(): + doc_now=doc_now+param.doc + doc_now+="\n" + return doc_now diff --git a/catbird/base.py b/catbird/base.py index 2168dcd..498e03e 100644 --- a/catbird/base.py +++ b/catbird/base.py @@ -84,8 +84,6 @@ def __setattr__(self, attr_name, value): def __init__(self): - print("In the MooseBase init method of ",self.__class__.__name__) - # List of the attributes this class should have moose_param_dict_local=getattr(self,self.params_name) @@ -170,11 +168,17 @@ def newattr(cls, attr_name, attr_type=str, dim=0, default=None, allowed_vals=Non raise RuntimeError(msg) # Set attribute docstring - doc_str = f'\nType: {attr_type.__name__}\n' - if description is not None: + doc_str = '\n' + doc_str += attr_name+' : ' + doc_str += f'{attr_type.__name__}\n' + if description is not None and description != "": + doc_str += " " doc_str += description + doc_str += "\n" if allowed_vals is not None: - doc_str += f'\nValues: {allowed_vals}' + doc_str += f' Allowed values: {allowed_vals}\n' + if default is not None: + doc_str += f' Default value: {default}\n' # Store parameter details in a structure moose_param=MooseParam() @@ -221,11 +225,4 @@ def attr_to_str(self,attr_name,print_default=False): return attr_str def moose_doc(self): - doc_now="Custom MooseBase docstring\n" - # moose_param_dict_local={} - # if hasattr(self,"_moose_params"): - # moose_param_dict_local=getattr(self,"_moose_params") - # for attr_name, param in moose_param_dict_local.items(): - # doc_now=doc_now+param.doc - # docs_now=super().__doc__+doc_now - return doc_now + pass diff --git a/catbird/factory.py b/catbird/factory.py index fd89c56..2bf6df9 100644 --- a/catbird/factory.py +++ b/catbird/factory.py @@ -98,7 +98,8 @@ def inner_to_str(self,print_default=False): @staticmethod def __get_docstring(mixins): - doc_now="Fa la la la la la la la la\n" + # Generate a docstring for new class from the docs of each mix-in + doc_now="" for base in mixins: doc_now=doc_now+base.moose_doc(base) return doc_now diff --git a/catbird/obj.py b/catbird/obj.py index 0a498cf..7ef7e4f 100644 --- a/catbird/obj.py +++ b/catbird/obj.py @@ -1,7 +1,6 @@ from .base import MooseBase class MooseObject(MooseBase): - """Tralalala""" params_name="_moose_params" def __init__(self): @@ -30,3 +29,25 @@ def inner_to_str(self,print_default=False): for attr_name in param_list: inner_str+=self.attr_to_str(attr_name,print_default) return inner_str + + def moose_doc(self): + doc_now='' + + # Documentation for all the parameters + moose_param_dict_local={} + if hasattr(self,"_moose_params"): + moose_param_dict_local=getattr(self,"_moose_params") + + if len(moose_param_dict_local.keys())>0: + doc_now='MOOSE Object Parameters\n' + doc_now+='-----------------------\n' + + # Formatting convention, start with type + if "type" in moose_param_dict_local.keys(): + param=moose_param_dict_local.pop("type") + doc_now=doc_now+param.doc + + for attr_name, param in moose_param_dict_local.items(): + doc_now=doc_now+param.doc + doc_now+="\n" + return doc_now From 1514482b8f8fff16e2e5f28f0fd65d07b760fdf5 Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Wed, 3 Jan 2024 14:12:27 +0000 Subject: [PATCH 47/62] Some refactoring and tidying --- catbird/base.py | 159 ++---------------------------------------- catbird/collection.py | 2 +- catbird/param.py | 35 ++++++++++ catbird/syntax.py | 19 ++--- 4 files changed, 53 insertions(+), 162 deletions(-) create mode 100644 catbird/param.py diff --git a/catbird/base.py b/catbird/base.py index 498e03e..a0ce8bf 100644 --- a/catbird/base.py +++ b/catbird/base.py @@ -1,63 +1,7 @@ from abc import ABC +from .param import MooseParam from .string import MooseString -class MooseParam(): - """ - Class to contain all information about a MOOSE parameter - """ - def __init__(self): - self.val=None - self.attr_type=None - self.default=None - self.allowed_vals=None - self.dim=0 - self.doc="" - -# class MyDescriptor: -# def __init__(self, value, attr_type, dim): -# self._value = value -# self._attr_type=attr_type -# self._dim=dim - -# def __get__(self, owner_instance, owner_type): -# print("We're getting from MyDescriptor") -# return owner_instance._value - -# def __set__(self, owner_instance, value): -# print("We're setting from MyDescriptor") -# value_to_set=value -# if isinstance(value,MyDescriptor): -# value_to_set=value._value - -# if self._.dim == 0: -# self.check_type(value_to_set,self._attr_type) -# # if param.allowed_vals is not None: -# # obj.check_vals(name, val, param.allowed_vals) -# else: -# val = np.asarray(value_to_set) -# self.check_type(val.flat[0].item(), self._attr_type) -# if len(val.shape) != param.dim: -# raise ValueError(f'Dimensionality is incorrect. Expects a {dim}-D array.') -# # for v in val.flatten(): -# # if param.allowed_vals is not None: -# # obj.check_vals(name, v, allowed_vals) -# self._value = value_to_set - -# @staticmethod -# def check_type(val, attr_type): -# """Checks a value's type""" -# if not isinstance(val, attr_type): -# val_type_str = val.__class__.__name__ -# exp_type_str = attr_type.__name__ -# raise ValueError(f'Incorrect type "{val_type_str}".' -# f'Expected type "{exp_type_str}".') - -# @staticmethod -# def check_vals(val, allowed_vals): -# """Checks that a value is in the set of allowed_values""" -# if val not in allowed_vals: -# raise ValueError(f'Value {val} is not one of {allowed_vals}') - class MooseBase(ABC,MooseString): """ Class that can add type-checked properties to itself. @@ -70,19 +14,6 @@ def __setattr__(self, attr_name, value): raise ValueError(msg) super().__setattr__(attr_name, value) - # def __getattribute__(self, attr): - # print("We're here") - # # If the attribute does not exist, super().__getattribute__() - # # will raise an AttributeError - # got_attr = super().__getattribute__(attr) - # try: - # # Try "manually" invoking the descriptor protocol __get__() - # return got_attr.__get__(self, type(self)) - # except AttributeError: - # # Attribute is not a descriptor, just return it: - # return got_attr - - def __init__(self): # List of the attributes this class should have moose_param_dict_local=getattr(self,self.params_name) @@ -92,13 +23,6 @@ def __init__(self): # Crucially, acts on the instance, not the class. setattr(self,attr_name,moose_param.val) - # print(" Adding property: ",attr_name) - - # # Property object (hopefully acts on OBJ) - # new_prop=self.moose_property(self,attr_name,moose_param) - # self.add_as_property(self,attr_name,new_prop) - - @staticmethod def check_type(name, val, attr_type): """Checks a value's type""" @@ -115,83 +39,16 @@ def check_vals(name, val, allowed_vals): if val not in allowed_vals: raise ValueError(f'Value {val} for attribute {name} is not one of {allowed_vals}') - # @staticmethod - # def moose_property(obj,name,param): - # """ - # Returns a property, and creates an associated class attribute whose value is that of the supplied MooseParam. - - # The property setter method will change the value of the underlying MooseParam.value, checking its type is consistent - # The property getter method will retrieve the value of the underlying MooseParam.value - # """ - - # def fget(self): - # # set to the default value if the internal attribute doesn't exist - # if not hasattr(obj, '_'+name): - # setattr(obj, '_'+name, param) - # param_now = getattr(obj, '_'+name) - # return param_now.val - - # def fset(self, val): - # print("setting", obj.lookup_name) - # if param.dim == 0: - # obj.check_type(name, val, param.attr_type) - # if param.allowed_vals is not None: - # obj.check_vals(name, val, param.allowed_vals) - # else: - # val = np.asarray(val) - # obj.check_type(name, val.flat[0].item(), param.attr_type) - # if len(val.shape) != param.dim: - # raise ValueError(f'Dimensionality is incorrect. Expects a {dim}-D array.') - # for v in val.flatten(): - # if param.allowed_vals is not None: - # obj.check_vals(name, v, allowed_vals) - - # param_now = getattr(obj, '_'+name) - # param_now.val=val - # setattr(obj, '_'+name, param_now) - - # def fdel(self): - # param_now = getattr(obj, '_'+name) - # del param_now - - - # return property(fget,None,fdel,param.doc) - @classmethod - def newattr(cls, attr_name, attr_type=str, dim=0, default=None, allowed_vals=None, description=None): - """Adds a property to the class""" - if not isinstance(attr_name, str): - raise ValueError('Attributes must be strings') + def add_moose_param(cls,moose_param): + assert isinstance(moose_param,MooseParam) + # Check name + attr_name=moose_param.name if attr_name.find("_syntax_") != -1: - msg="'_syntax_' is reserved attribute string. Cannot add attibute {}".format(attr_name) + msg="'_syntax_' is reserved attribute string. Cannot create attibute {}".format(attr_name) raise RuntimeError(msg) - # Set attribute docstring - doc_str = '\n' - doc_str += attr_name+' : ' - doc_str += f'{attr_type.__name__}\n' - if description is not None and description != "": - doc_str += " " - doc_str += description - doc_str += "\n" - if allowed_vals is not None: - doc_str += f' Allowed values: {allowed_vals}\n' - if default is not None: - doc_str += f' Default value: {default}\n' - - # Store parameter details in a structure - moose_param=MooseParam() - if default is not None: - moose_param.val=default - else: - moose_param.val=attr_type() - moose_param.attr_type=attr_type - moose_param.default=default - moose_param.dim=dim - moose_param.allowed_vals=allowed_vals - moose_param.doc=doc_str - # Store attribute in dict params_name=None if cls.params_name: @@ -202,10 +59,6 @@ def newattr(cls, attr_name, attr_type=str, dim=0, default=None, allowed_vals=Non setattr(cls,cls.params_name,moose_param_dict_local) - @staticmethod - def add_as_property(obj,attr_name,property_obj): - setattr(obj, attr_name, property_obj) - def is_default(self,attr_name): attr_val = getattr(self, attr_name) param = getattr(self, "_"+attr_name) diff --git a/catbird/collection.py b/catbird/collection.py index 9c17314..5c2dcd5 100644 --- a/catbird/collection.py +++ b/catbird/collection.py @@ -29,7 +29,7 @@ def add(self,obj,lookup_name): # Set the name of the object obj.set_lookup_name(lookup_name) - # Don't dupicate entries in collection + # Don't duplicate entries in collection if lookup_name in self.objects.keys(): msg="Collection already contains named block {}".format(block_name) raise RuntimeError(msg) diff --git a/catbird/param.py b/catbird/param.py new file mode 100644 index 0000000..cb8cd65 --- /dev/null +++ b/catbird/param.py @@ -0,0 +1,35 @@ +class MooseParam(): + """ + Class to contain all information about a MOOSE parameter + """ + def __init__(self,attr_name, attr_type, dim=0, default=None, allowed_vals=None, description=None): + self.attr_type=attr_type + self.default=default + self.allowed_vals=allowed_vals + self.dim=dim + + # Set name + if not isinstance(attr_name, str): + raise ValueError('Attribute names must be strings') + self.name=attr_name + + # Set value + if default is not None: + self.val=default + else: + self.val=attr_type() + + # Set docstring + doc_str = '\n' + doc_str += attr_name+' : ' + doc_str += f'{attr_type.__name__}\n' + if description is not None and description != "": + doc_str += " " + doc_str += description + doc_str += "\n" + if allowed_vals is not None: + doc_str += f' Allowed values: {allowed_vals}\n' + if default is not None: + doc_str += f' Default value: {default}\n' + + self.doc=doc_str diff --git a/catbird/syntax.py b/catbird/syntax.py index 41083b5..8489078 100644 --- a/catbird/syntax.py +++ b/catbird/syntax.py @@ -3,6 +3,7 @@ from copy import deepcopy from .obj import MooseObject from .action import MooseAction +from .param import MooseParam from .collection import * type_mapping = {'Integer' : int, @@ -419,7 +420,7 @@ def parse_block(json_obj,syntax_path,class_name): # Available syntax for this block as dict block=fetch_syntax(json_obj,syntax_path) - # Create new subclass of MooseObject with a name that matches the block + # Create new subclass with a name that matches the block name=class_name # Deduce type of object by its relation to parent @@ -460,13 +461,15 @@ def parse_block(json_obj,syntax_path,class_name): else: default = [_convert_to_type(attr_type, v) for v in param_info['default'].split()] - # Add an attribute to the class instance for this parameter - new_cls.newattr(param_name, - attr_type, - description=param_info.get('description'), - default=default, - dim=ndim, - allowed_vals=allowed_values) + # Create and add a MOOSE parameter + moose_param=MooseParam(param_name, + attr_type, + description=param_info.get('description'), + default=default, + dim=ndim, + allowed_vals=allowed_values) + + new_cls.add_moose_param(moose_param) # Return our new class From cb00d4f340c852d95b0ca58e005467a3a1d39857 Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Wed, 3 Jan 2024 15:50:16 +0000 Subject: [PATCH 48/62] Documentation works for MooseCollection objects --- catbird/action.py | 23 +---------------------- catbird/base.py | 32 ++++++++++++++++++++++++++++++-- catbird/factory.py | 9 ++------- catbird/obj.py | 23 +---------------------- catbird/syntax.py | 39 +++++++++++++++++++++++++++------------ 5 files changed, 61 insertions(+), 65 deletions(-) diff --git a/catbird/action.py b/catbird/action.py index a652892..f32d5b6 100644 --- a/catbird/action.py +++ b/catbird/action.py @@ -2,6 +2,7 @@ class MooseAction(MooseBase): params_name="_moose_action_params" + class_alias="Action" def __init__(self): super().__init__() @@ -28,25 +29,3 @@ def inner_to_str(self,print_default=False): for attr_name in param_list: inner_str+=self.attr_to_str(attr_name,print_default) return inner_str - - - def moose_doc(self): - doc_now='' - - # Documentation for all the parameters - moose_param_dict_local={} - if hasattr(self,"_moose_action_params"): - moose_param_dict_local=getattr(self,"_moose_action_params") - - # We don't print the type of actions - if "type" in moose_param_dict_local.keys(): - param=moose_param_dict_local.pop("type") - - if len(moose_param_dict_local.keys())>0: - doc_now='MOOSE Action Parameters\n' - doc_now+='-----------------------n' - - for attr_name, param in moose_param_dict_local.items(): - doc_now=doc_now+param.doc - doc_now+="\n" - return doc_now diff --git a/catbird/base.py b/catbird/base.py index a0ce8bf..7df9fb4 100644 --- a/catbird/base.py +++ b/catbird/base.py @@ -77,5 +77,33 @@ def attr_to_str(self,attr_name,print_default=False): attr_str=self.indent+'{}={}\n'.format(attr_name,attr_val) return attr_str - def moose_doc(self): - pass + @classmethod + def moose_doc(cls, param_list): + """Generate documentation for all the MOOSE parameters""" + # Obtain class info for header + class_string='' + if hasattr(cls,'class_alias'): + class_name_now=getattr(cls,'class_alias') + class_string=" "+class_name_now + param_string='MOOSE'+class_string+' Parameters' + dash_len=len(param_string) + dashes=''.ljust(dash_len,"-") + + # Write header + doc_now=param_string + doc_now+="\n"+dashes + + # Loop over parameters' documentation + for param in param_list: + assert isinstance(param,MooseParam) + # We don't document the type as it is fixed + if param.name == "type": + continue + doc_now=doc_now+param.doc + + # Footer + more_dashes=''.ljust(65,"-") + doc_now+=more_dashes + doc_now+="\n" + + return doc_now diff --git a/catbird/factory.py b/catbird/factory.py index 2bf6df9..242927d 100644 --- a/catbird/factory.py +++ b/catbird/factory.py @@ -101,7 +101,8 @@ def __get_docstring(mixins): # Generate a docstring for new class from the docs of each mix-in doc_now="" for base in mixins: - doc_now=doc_now+base.moose_doc(base) + if base.__doc__ is not None: + doc_now=doc_now+base.__doc__ return doc_now def derive_class(self,root_name,obj_types): @@ -142,10 +143,6 @@ def derive_class(self,root_name,obj_types): # Convert to tuple mixin_tuple=tuple(mixin_list) - - #doc_str_now=self.__get_docstring(mixin_tuple) - #print(doc_str_now) - # Our fancy new mixin class new_cls = type(root_name, mixin_tuple, { @@ -177,8 +174,6 @@ def construct_root(self,root_name,obj_types,kwargs): return obj def construct(self,root_name,relation_type,derived_type, **kwargs): - print(root_name,relation_type,derived_type) - class_now=self.constructors[root_name][relation_type][derived_type] obj=class_now() # Handle keyword arguments diff --git a/catbird/obj.py b/catbird/obj.py index 7ef7e4f..c647235 100644 --- a/catbird/obj.py +++ b/catbird/obj.py @@ -2,6 +2,7 @@ class MooseObject(MooseBase): params_name="_moose_params" + class_alias="Object" def __init__(self): super().__init__() @@ -29,25 +30,3 @@ def inner_to_str(self,print_default=False): for attr_name in param_list: inner_str+=self.attr_to_str(attr_name,print_default) return inner_str - - def moose_doc(self): - doc_now='' - - # Documentation for all the parameters - moose_param_dict_local={} - if hasattr(self,"_moose_params"): - moose_param_dict_local=getattr(self,"_moose_params") - - if len(moose_param_dict_local.keys())>0: - doc_now='MOOSE Object Parameters\n' - doc_now+='-----------------------\n' - - # Formatting convention, start with type - if "type" in moose_param_dict_local.keys(): - param=moose_param_dict_local.pop("type") - doc_now=doc_now+param.doc - - for attr_name, param in moose_param_dict_local.items(): - doc_now=doc_now+param.doc - doc_now+="\n" - return doc_now diff --git a/catbird/syntax.py b/catbird/syntax.py index 8489078..2525ab8 100644 --- a/catbird/syntax.py +++ b/catbird/syntax.py @@ -417,22 +417,37 @@ def fetch_syntax(json_dict,syntax): return dict_now def parse_block(json_obj,syntax_path,class_name): - # Available syntax for this block as dict - block=fetch_syntax(json_obj,syntax_path) - - # Create new subclass with a name that matches the block - name=class_name + # Construct Moose parameters + moose_param_list=get_params_list(json_obj,syntax_path) # Deduce type of object by its relation to parent relation=_relation_shorthands[syntax_path.parent_relation] class_type=_child_type_map[relation] - #print(class_type().moose_doc) - new_cls = type(name, - (class_type,),dict()) + # Generate class documentation + doc_now=class_type.moose_doc(moose_param_list) + + # Create new class with a name that matches the block + new_cls = type(class_name, + (class_type,), + { + "__doc__": doc_now, + }) - # Add parameters as attributes + # Add the parameters to the class + for moose_param in moose_param_list: + new_cls.add_moose_param(moose_param) + + # Return our new class + return new_cls + +def get_params_list(json_obj,syntax_path): + # Available syntax for this block as dict + block=fetch_syntax(json_obj,syntax_path) + + # Lift parameters dictionary params=block["parameters"] + moose_param_list=[] for param_name, param_info in params.items(): # Determine the type of the parameter attr_types = tuple(type_mapping[t] for t in param_info['basic_type'].split(':')) @@ -469,11 +484,11 @@ def parse_block(json_obj,syntax_path,class_name): dim=ndim, allowed_vals=allowed_values) - new_cls.add_moose_param(moose_param) + moose_param_list.append(moose_param) + # Return list + return moose_param_list - # Return our new class - return new_cls # convenience function for converting types def _convert_to_type(t, val): From 16cd4fd1832c5a11ee229d312437cb328d1dc73a Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Wed, 3 Jan 2024 16:17:53 +0000 Subject: [PATCH 49/62] Re-enable support for factory config file --- catbird/factory.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/catbird/factory.py b/catbird/factory.py index 242927d..be1425c 100644 --- a/catbird/factory.py +++ b/catbird/factory.py @@ -7,23 +7,20 @@ class Factory(): def __init__(self,exec_path,config_file=None): print("Loading syntax from library...") json_obj=json_from_exec(exec_path) - print("Done.") print("Constructing syntax registry...") self.registry=SyntaxRegistry(json_obj) - print("Done.") - - self.available_blocks=self.registry.get_available_blocks() print("Configuring objects to enable...") + self.available_blocks=self.registry.get_available_blocks() self.set_defaults() - print("Done.") - # if config_file is not None: - # self.load_config(config_file) + if config_file is not None: + print("Loading configuration from file",config_file) + self.load_config(config_file) + print("Loading enabled objects...") self.load_enabled_objects(json_obj) - print("Done.") def _load_root_syntax(self,block_name, block): """ @@ -240,7 +237,6 @@ def load_config(self,filename): self.available_blocks[block_name].enabled=block_dict["enabled"] def set_defaults(self): - #self.enable_syntax("Mesh") mesh_enable_dict={ "action": ["CreateDisplacedProblemAction", "DisplayGhostingAction"], From 239fbd8a706c63f2dfedbfe35ce4479a1f02e4fb Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Thu, 4 Jan 2024 10:22:53 +0000 Subject: [PATCH 50/62] fix mix-in initialisation --- catbird/action.py | 9 ++++++++- catbird/base.py | 22 +++++++++++++--------- catbird/factory.py | 43 +++++++++++++++++++++++++++++++++++++++++-- catbird/obj.py | 21 +++++++++++++++------ 4 files changed, 77 insertions(+), 18 deletions(-) diff --git a/catbird/action.py b/catbird/action.py index f32d5b6..6a90473 100644 --- a/catbird/action.py +++ b/catbird/action.py @@ -5,7 +5,14 @@ class MooseAction(MooseBase): class_alias="Action" def __init__(self): - super().__init__() + if hasattr(self,"_action_moose_params"): + # Dictionary of the attributes this class should have + moose_param_dict_local=getattr(self,"_moose_action_params") + + # Loop over and make into attributes + for attr_name, moose_param in moose_param_dict_local.items(): + # Crucially, acts on the instance, not the class. + setattr(self,attr_name,moose_param.val) @property def moose_action_params(self): diff --git a/catbird/base.py b/catbird/base.py index 7df9fb4..e3e05c3 100644 --- a/catbird/base.py +++ b/catbird/base.py @@ -14,14 +14,6 @@ def __setattr__(self, attr_name, value): raise ValueError(msg) super().__setattr__(attr_name, value) - def __init__(self): - # List of the attributes this class should have - moose_param_dict_local=getattr(self,self.params_name) - - # Loop over and make into properties - for attr_name, moose_param in moose_param_dict_local.items(): - # Crucially, acts on the instance, not the class. - setattr(self,attr_name,moose_param.val) @staticmethod def check_type(name, val, attr_type): @@ -58,13 +50,25 @@ def add_moose_param(cls,moose_param): moose_param_dict_local[attr_name]=moose_param setattr(cls,cls.params_name,moose_param_dict_local) + def get_param(self,attr_name): + """Return MooseParam corresponding to attribute name. + + Raise KeyError if not found. + """ + dict_now=getattr(self,self.params_name) + return dict_now[attr_name] def is_default(self,attr_name): + # Get current value attr_val = getattr(self, attr_name) - param = getattr(self, "_"+attr_name) + + # Look up the default value + param=self.get_param(attr_name) default_val = param.default if default_val is None: default_val = param.attr_type() + + # Compare and return return attr_val == default_val def attr_to_str(self,attr_name,print_default=False): diff --git a/catbird/factory.py b/catbird/factory.py index be1425c..2d5234b 100644 --- a/catbird/factory.py +++ b/catbird/factory.py @@ -77,10 +77,26 @@ def load_enabled_objects(self,json_obj): @staticmethod def __get_init_method(mixins): - # The returned init method should call each of the mix-in base class init methods in turn. def __init__(self, *args, **kwargs): for base in mixins: - base.__init__(self, *args, **kwargs) + # This mix-in does not have parameters at all + if not hasattr(base,"params_name"): + base.__init__(self, *args, **kwargs) + continue + + # Might not have enabled any parameters for this mix-in + if not hasattr(self,base.params_name): + continue + + # Dictionary of the attributes this base class should have + moose_param_dict_local=getattr(self,base.params_name) + + # Loop over and make into properties + for attr_name, moose_param in moose_param_dict_local.items(): + # Crucially, acts on the instance, not the class. + setattr(self,attr_name,moose_param.val) + #Todo: apply kwargs + return __init__ @staticmethod @@ -93,6 +109,28 @@ def inner_to_str(self,print_default=False): return inner_str return inner_to_str + @staticmethod + def __get_get_param_method(mixins): + def get_param(self,attr_name): + # Search each base in turn + param=None + for base in mixins: + # Skip mix-ins with no parameters + if not hasattr(base,"params_name"): + continue + try: + dict_now=getattr(self,base.params_name) + param=dict_now[attr_name] + break + except KeyError: + continue + + if param is None: + msg="Could not find MOOSE param for attribute {}".format(attr_name) + raise KeyError(msg) + return param + return get_param + @staticmethod def __get_docstring(mixins): # Generate a docstring for new class from the docs of each mix-in @@ -146,6 +184,7 @@ def derive_class(self,root_name,obj_types): "__init__": self.__get_init_method(mixin_tuple), "__doc__": self.__get_docstring(mixin_tuple), "inner_to_str":self.__get_inner_to_str_method(mixin_tuple), + "get_param":self.__get_get_param_method(mixin_tuple), }) return new_cls diff --git a/catbird/obj.py b/catbird/obj.py index c647235..2404a0a 100644 --- a/catbird/obj.py +++ b/catbird/obj.py @@ -1,3 +1,4 @@ +from copy import deepcopy from .base import MooseBase class MooseObject(MooseBase): @@ -5,7 +6,15 @@ class MooseObject(MooseBase): class_alias="Object" def __init__(self): - super().__init__() + if hasattr(self,"_moose_params"): + # Dictionary of the attributes this class should have + moose_param_dict_local=getattr(self,"_moose_params") + + # Loop over and make into attributes + for attr_name, moose_param in moose_param_dict_local.items(): + # Crucially, acts on the instance, not the class. + setattr(self,attr_name,moose_param.val) + @property def moose_object_params(self): @@ -20,13 +29,13 @@ def moose_object_params(self): def inner_to_str(self,print_default=False): inner_str="" - param_list=self.moose_object_params + param_list_local=deepcopy(self.moose_object_params) # Formatting convention, start with type - if "type" in param_list: - param_list.remove("type") - inner_str+=self.attr_to_str("type",True) + if "type" in param_list_local: + param_list_local.remove("type") + inner_str+=self.attr_to_str("type",True) - for attr_name in param_list: + for attr_name in param_list_local: inner_str+=self.attr_to_str(attr_name,print_default) return inner_str From 33767530c77c1d8eba8a8ec2bf383091e50f9f29 Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Tue, 6 Feb 2024 09:44:13 +0000 Subject: [PATCH 51/62] fix bugs lifting syntax from json tree, and when setting attributes attempt to cast as correct type --- catbird/base.py | 14 +++++++++----- catbird/syntax.py | 14 ++++++++------ 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/catbird/base.py b/catbird/base.py index e3e05c3..3712d00 100644 --- a/catbird/base.py +++ b/catbird/base.py @@ -6,13 +6,17 @@ class MooseBase(ABC,MooseString): """ Class that can add type-checked properties to itself. """ - def __setattr__(self, attr_name, value): + def __setattr__(self, attr_name, value_in): + value_to_set=value_in if hasattr(self,attr_name): type_now=type(getattr(self,attr_name)) - if not isinstance(value,type_now): - msg="Attribute {} should have type {}".format(attr_name,type_now) - raise ValueError(msg) - super().__setattr__(attr_name, value) + if not isinstance(value_in,type_now): + try: + value_to_set=type_now(value_in) + except ValueError: + msg="Attribute {} should have type {}".format(attr_name,type_now) + raise ValueError(msg) + super().__setattr__(attr_name, value_to_set) @staticmethod diff --git a/catbird/syntax.py b/catbird/syntax.py index 2525ab8..76651b0 100644 --- a/catbird/syntax.py +++ b/catbird/syntax.py @@ -47,7 +47,6 @@ "nested_collection_type": None, # Don't support this syntax yet } - def get_relation_kwargs(): return _relation_shorthands.values() @@ -106,7 +105,6 @@ def __init__(self, syntax_path_in): if relation_path: _parent_relation=self._key_from_list(relation_path) if _parent_relation not in _relation_shorthands.keys(): - print(syntax_path_in) raise RuntimeError("unknown relation type: {}",format(_parent_relation)) self.parent_relation=_parent_relation @@ -448,8 +446,8 @@ def get_params_list(json_obj,syntax_path): # Lift parameters dictionary params=block["parameters"] moose_param_list=[] + for param_name, param_info in params.items(): - # Determine the type of the parameter attr_types = tuple(type_mapping[t] for t in param_info['basic_type'].split(':')) attr_type = attr_types[-1] @@ -471,10 +469,14 @@ def get_params_list(json_obj,syntax_path): # properties in the type definition as they are now default = None if 'default' in param_info.keys() and param_info['default'] != None and param_info['default'] != '': - if ndim == 0: - default = _convert_to_type(attr_type, param_info['default']) + if ndim > 0 and attr_type != str : + defaults = param_info['default'] + if type(defaults) == str: + default = [_convert_to_type(attr_type, v) for v in defaults.split()] + else: + default = [_convert_to_type(attr_type, v) for v in defaults] else: - default = [_convert_to_type(attr_type, v) for v in param_info['default'].split()] + default = _convert_to_type(attr_type, param_info['default']) # Create and add a MOOSE parameter moose_param=MooseParam(param_name, From 9d3a15f9f5c8052a0f96e1f72d5a63a2fc9759cf Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Tue, 6 Feb 2024 12:09:19 +0000 Subject: [PATCH 52/62] support mixin types as leaf nodes --- catbird/factory.py | 33 ++++++++++++++++----------- catbird/model.py | 56 +++++++++++++++++++++++++++++++++------------- 2 files changed, 61 insertions(+), 28 deletions(-) diff --git a/catbird/factory.py b/catbird/factory.py index 2d5234b..99de535 100644 --- a/catbird/factory.py +++ b/catbird/factory.py @@ -7,20 +7,26 @@ class Factory(): def __init__(self,exec_path,config_file=None): print("Loading syntax from library...") json_obj=json_from_exec(exec_path) + print("Done") print("Constructing syntax registry...") self.registry=SyntaxRegistry(json_obj) + self.available_blocks=self.registry.get_available_blocks() + print("Done") print("Configuring objects to enable...") - self.available_blocks=self.registry.get_available_blocks() self.set_defaults() + print("Done") if config_file is not None: print("Loading configuration from file",config_file) self.load_config(config_file) + print("Done") print("Loading enabled objects...") self.load_enabled_objects(json_obj) + print("Done") + def _load_root_syntax(self,block_name, block): """ @@ -157,15 +163,9 @@ def derive_class(self,root_name,obj_types): for relation_type,derived_type in obj_types.items(): if relation_type not in mixins.keys(): raise RuntimeError("{} is not an available mix-in".format(relation_type)) - mixin_base_class=mixins[relation_type] - # Fetch derived class for mix-in class_now=self.constructors[root_name][relation_type][derived_type] - # Check provided object type is of the correct type - if not issubclass(class_now,mixin_base_class): - raise RuntimeError("{} has wrong base class".format(derived_type)) - # Update mixins_now[relation_type]=class_now @@ -209,13 +209,14 @@ def construct_root(self,root_name,obj_types,kwargs): return obj - def construct(self,root_name,relation_type,derived_type, **kwargs): - class_now=self.constructors[root_name][relation_type][derived_type] + def construct(self,root_name,obj_types, **kwargs): + class_now=self.derive_class(root_name,obj_types) obj=class_now() + # Handle keyword arguments for key, value in kwargs.items(): if not hasattr(obj,key): - msg="Object type {} does not have attribute {}".format(root_name,key) + msg="Object type {} does not have attribute {}".format(derived_type,key) raise RuntimeError(msg) setattr(obj, key, value) return obj @@ -277,11 +278,17 @@ def load_config(self,filename): def set_defaults(self): mesh_enable_dict={ - "action": ["CreateDisplacedProblemAction", - "DisplayGhostingAction"], "obj_type": ["FileMesh","GeneratedMesh"] } self.enable_syntax("Mesh",enable_dict=mesh_enable_dict) - self.enable_syntax("Executioner") + + executioner_enable_dict={ + "obj_type": ["Steady","Transient"] + } + self.enable_syntax("Executioner", executioner_enable_dict) self.enable_syntax("Problem") self.enable_syntax("Variables") + self.enable_syntax("Kernels") + self.enable_syntax("Materials") + self.enable_syntax("BCs") + self.enable_syntax("Outputs") diff --git a/catbird/model.py b/catbird/model.py index 5b6f573..d031f6f 100644 --- a/catbird/model.py +++ b/catbird/model.py @@ -17,12 +17,17 @@ def __init__(self,factory_in): # Envisage this being overridden downstream. def load_default_syntax(self): self.add_syntax("Executioner", obj_type="Steady") - self.add_syntax("Executioner.Predictor",obj_type="AdamsPredictor") + #self.add_syntax("Executioner.Predictor",obj_type="AdamsPredictor") self.add_syntax("Problem", obj_type="FEProblem") self.add_syntax("Mesh", obj_type="GeneratedMesh", action="CreateDisplacedProblemAction") self.add_syntax("Variables") + self.add_syntax("Kernels") + self.add_syntax("Materials") + self.add_syntax("BCs") + self.add_syntax("Outputs") + def add_syntax(self,syntax_name,**kwargs_in): """ @@ -84,31 +89,21 @@ def _add_to_model(self,obj): def add_to_collection(self, collection_name, object_name,**kwargs_in): # First, pop out any relation key-word args - obj_type_kwargs={} + obj_types={} relations=get_relation_kwargs() kwargs=deepcopy(kwargs_in) for keyword in kwargs_in.keys(): if keyword in relations: obj_type_value = kwargs.pop(keyword) - obj_type_kwargs[keyword]=obj_type_value + obj_types[keyword]=obj_type_value - relations=list(obj_type_kwargs.keys()) - obj_classes=list(obj_type_kwargs.values()) - - # If more than one type, error! - if len(relations) > 1: - msg="Cannot add mixin types to collection" - raise RuntimeError(msg) # One basic type is mandatory - if len(relations) == 0: + if len(obj_types) == 0: msg="Must specify a relation type" raise RuntimeError(msg) - relation=relations[0] - obj_class_name=obj_classes[0] - - obj=self.factory.construct(collection_name,relation,obj_class_name,**kwargs) + obj=self.factory.construct(collection_name,obj_types,**kwargs) # Fetch collection and add collection = getattr(self, collection_name.lower()) @@ -118,8 +113,25 @@ def add_to_collection(self, collection_name, object_name,**kwargs_in): def add_variable(self,variable_name,variable_type="MooseVariable",**kwargs_in): collection_kwargs=deepcopy(kwargs_in) collection_kwargs["collection_type"]=variable_type + collection_kwargs["collection_action"]="AddVariableAction" + self.add_to_collection("Variables",variable_name,**collection_kwargs) + def add_kernel(self,kernel_name,kernel_type,**kwargs_in): + collection_kwargs=deepcopy(kwargs_in) + collection_kwargs["collection_type"]=kernel_type + self.add_to_collection("Kernels",kernel_name,**collection_kwargs) + + def add_bc(self,bc_name,bc_type,**kwargs_in): + collection_kwargs=deepcopy(kwargs_in) + collection_kwargs["collection_type"]=bc_type + self.add_to_collection("BCs",bc_name,**collection_kwargs) + + def add_material(self,mat_name,mat_type,**kwargs_in): + collection_kwargs=deepcopy(kwargs_in) + collection_kwargs["collection_type"]=mat_type + self.add_to_collection("Materials",mat_name,**collection_kwargs) + def to_str(self,print_default=False): model_str="" for obj_type in self.moose_objects: @@ -132,3 +144,17 @@ def write(self, filename): file_handle.write(self.to_str()) file_handle.close() print("Wrote to ",filename) + +class TransientModel(MooseModel): + """Class to represent a MOOSE model""" + def __init__(self,factory_in): + super().__init__(factory_in) + + def load_default_syntax(self): + self.add_syntax("Executioner", obj_type="Transient") + self.add_syntax("Mesh", obj_type="GeneratedMesh") + self.add_syntax("Variables") + #self.add_syntax("Kernels") + #self.add_syntax("Materials") + #self.add_syntax("BCs") + #self.add_syntax("Outputs") From 58bb98e132ca547f22254c556c2ddc4d53423b95 Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Tue, 6 Feb 2024 14:13:13 +0000 Subject: [PATCH 53/62] avoid syntax collisions for mixins --- catbird/factory.py | 4 ++++ catbird/model.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/catbird/factory.py b/catbird/factory.py index 99de535..225289c 100644 --- a/catbird/factory.py +++ b/catbird/factory.py @@ -100,6 +100,10 @@ def __init__(self, *args, **kwargs): # Loop over and make into properties for attr_name, moose_param in moose_param_dict_local.items(): # Crucially, acts on the instance, not the class. + if hasattr(self,attr_name): + msg="Warning! Syntax collision for attribute {} in class {}. Skipping.".format(attr_name, self.__class__.__name__) + print(msg) + continue setattr(self,attr_name,moose_param.val) #Todo: apply kwargs diff --git a/catbird/model.py b/catbird/model.py index d031f6f..6e1534b 100644 --- a/catbird/model.py +++ b/catbird/model.py @@ -154,7 +154,7 @@ def load_default_syntax(self): self.add_syntax("Executioner", obj_type="Transient") self.add_syntax("Mesh", obj_type="GeneratedMesh") self.add_syntax("Variables") - #self.add_syntax("Kernels") + self.add_syntax("Kernels") #self.add_syntax("Materials") #self.add_syntax("BCs") #self.add_syntax("Outputs") From 5af584f54d61366d429c9deb94c2f81294227d25 Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Wed, 7 Feb 2024 11:37:22 +0000 Subject: [PATCH 54/62] better handling of default values, indents, and type-casting of attributes --- catbird/base.py | 41 ++++++++++++++++++++++++++++++++++++----- catbird/factory.py | 16 +++++++++------- catbird/model.py | 17 +++++++++-------- catbird/param.py | 19 +++++++++++++------ catbird/syntax.py | 18 ++++++++++-------- 5 files changed, 77 insertions(+), 34 deletions(-) diff --git a/catbird/base.py b/catbird/base.py index 3712d00..6b223a0 100644 --- a/catbird/base.py +++ b/catbird/base.py @@ -1,4 +1,5 @@ from abc import ABC +from collections.abc import Iterable from .param import MooseParam from .string import MooseString @@ -9,10 +10,27 @@ class MooseBase(ABC,MooseString): def __setattr__(self, attr_name, value_in): value_to_set=value_in if hasattr(self,attr_name): - type_now=type(getattr(self,attr_name)) + attr_val_now=getattr(self,attr_name) + type_now=type(attr_val_now) + + sub_type_now=None + if issubclass(type_now, Iterable) and type_now != str : + sub_type_now=type(attr_val_now[0]) + if not isinstance(value_in,type_now): + # If not the right type, try to cast + values=None + if isinstance(value_in,str): + values=value_in.split() + try: - value_to_set=type_now(value_in) + if sub_type_now is not None: + if values: + value_to_set=[ sub_type_now(v) for v in values ] + else: + value_to_set=[sub_type_now(value_in)] + else: + value_to_set=type_now(value_in) except ValueError: msg="Attribute {} should have type {}".format(attr_name,type_now) raise ValueError(msg) @@ -70,6 +88,7 @@ def is_default(self,attr_name): param=self.get_param(attr_name) default_val = param.default if default_val is None: + default_val = param.attr_type() # Compare and return @@ -80,9 +99,21 @@ def attr_to_str(self,attr_name,print_default=False): if self.is_default(attr_name) and not print_default: return attr_str - attr_val = getattr(self, attr_name) - if attr_val is not None: - attr_str=self.indent+'{}={}\n'.format(attr_name,attr_val) + attr_val=getattr(self, attr_name) + if attr_val is None: + return attr_str + + type_now=type(attr_val) + attr_val_str="" + if issubclass(type_now, Iterable) and type_now != str : + str_list = [ str(v)+" " for v in attr_val ] + attr_val_str="".join(str_list) + attr_val_str=attr_val_str.rstrip() + attr_val_str="'"+attr_val_str+"'" + else: + attr_val_str=str(attr_val) + + attr_str=self.indent+'{}='.format(attr_name)+attr_val_str+'\n' return attr_str @classmethod diff --git a/catbird/factory.py b/catbird/factory.py index 225289c..7dc7acd 100644 --- a/catbird/factory.py +++ b/catbird/factory.py @@ -150,7 +150,7 @@ def __get_docstring(mixins): doc_now=doc_now+base.__doc__ return doc_now - def derive_class(self,root_name,obj_types): + def derive_class(self,root_name,obj_types,class_name): """ Form a new mix-in class from a tuple of classes @@ -183,7 +183,7 @@ def derive_class(self,root_name,obj_types): mixin_tuple=tuple(mixin_list) # Our fancy new mixin class - new_cls = type(root_name, mixin_tuple, + new_cls = type(class_name, mixin_tuple, { "__init__": self.__get_init_method(mixin_tuple), "__doc__": self.__get_docstring(mixin_tuple), @@ -201,7 +201,7 @@ def construct_root(self,root_name,obj_types,kwargs): kwargs: dict """ # Get class - obj_class=self.derive_class(root_name, obj_types) + obj_class=self.derive_class(root_name, obj_types,root_name) obj=obj_class() # Handle keyword arguments @@ -213,8 +213,8 @@ def construct_root(self,root_name,obj_types,kwargs): return obj - def construct(self,root_name,obj_types, **kwargs): - class_now=self.derive_class(root_name,obj_types) + def construct(self,root_name,obj_types,class_name,**kwargs): + class_now=self.derive_class(root_name,obj_types,class_name) obj=class_now() # Handle keyword arguments @@ -293,6 +293,8 @@ def set_defaults(self): self.enable_syntax("Problem") self.enable_syntax("Variables") self.enable_syntax("Kernels") - self.enable_syntax("Materials") self.enable_syntax("BCs") - self.enable_syntax("Outputs") + self.enable_syntax("Materials") + self.enable_syntax("VectorPostprocessors") + + #self.enable_syntax("Outputs") diff --git a/catbird/model.py b/catbird/model.py index 6e1534b..92a145d 100644 --- a/catbird/model.py +++ b/catbird/model.py @@ -87,7 +87,7 @@ def _add_to_model(self,obj): if parent_obj == self: self.moose_objects.append(attr_name) - def add_to_collection(self, collection_name, object_name,**kwargs_in): + def add_to_collection(self, collection_name, class_name, object_name,**kwargs_in): # First, pop out any relation key-word args obj_types={} relations=get_relation_kwargs() @@ -103,7 +103,8 @@ def add_to_collection(self, collection_name, object_name,**kwargs_in): msg="Must specify a relation type" raise RuntimeError(msg) - obj=self.factory.construct(collection_name,obj_types,**kwargs) + long_class_name=collection_name+"."+class_name + obj=self.factory.construct(collection_name,obj_types,long_class_name,**kwargs) # Fetch collection and add collection = getattr(self, collection_name.lower()) @@ -115,22 +116,22 @@ def add_variable(self,variable_name,variable_type="MooseVariable",**kwargs_in): collection_kwargs["collection_type"]=variable_type collection_kwargs["collection_action"]="AddVariableAction" - self.add_to_collection("Variables",variable_name,**collection_kwargs) + self.add_to_collection("Variables","Variable",variable_name,**collection_kwargs) def add_kernel(self,kernel_name,kernel_type,**kwargs_in): collection_kwargs=deepcopy(kwargs_in) collection_kwargs["collection_type"]=kernel_type - self.add_to_collection("Kernels",kernel_name,**collection_kwargs) + self.add_to_collection("Kernels","Kernel",kernel_name,**collection_kwargs) def add_bc(self,bc_name,bc_type,**kwargs_in): collection_kwargs=deepcopy(kwargs_in) collection_kwargs["collection_type"]=bc_type - self.add_to_collection("BCs",bc_name,**collection_kwargs) + self.add_to_collection("BCs","BC", bc_name,**collection_kwargs) def add_material(self,mat_name,mat_type,**kwargs_in): collection_kwargs=deepcopy(kwargs_in) collection_kwargs["collection_type"]=mat_type - self.add_to_collection("Materials",mat_name,**collection_kwargs) + self.add_to_collection("Materials","Material",mat_name,**collection_kwargs) def to_str(self,print_default=False): model_str="" @@ -155,6 +156,6 @@ def load_default_syntax(self): self.add_syntax("Mesh", obj_type="GeneratedMesh") self.add_syntax("Variables") self.add_syntax("Kernels") - #self.add_syntax("Materials") - #self.add_syntax("BCs") + self.add_syntax("BCs") + self.add_syntax("Materials") #self.add_syntax("Outputs") diff --git a/catbird/param.py b/catbird/param.py index cb8cd65..11dbdd6 100644 --- a/catbird/param.py +++ b/catbird/param.py @@ -2,22 +2,29 @@ class MooseParam(): """ Class to contain all information about a MOOSE parameter """ - def __init__(self,attr_name, attr_type, dim=0, default=None, allowed_vals=None, description=None): + def __init__(self,attr_name, attr_type, is_array, default=None, allowed_vals=None, description=None): + + assert attr_type is not type(None) + self.attr_type=attr_type - self.default=default self.allowed_vals=allowed_vals - self.dim=dim + self.is_array=is_array # Set name if not isinstance(attr_name, str): raise ValueError('Attribute names must be strings') self.name=attr_name - # Set value + # Set default value if default is not None: - self.val=default + self.default=default + elif self.is_array and attr_type!=str: + self.default=[attr_type()] else: - self.val=attr_type() + self.default=attr_type() + + # Initialise current value to the default + self.val=self.default # Set docstring doc_str = '\n' diff --git a/catbird/syntax.py b/catbird/syntax.py index 76651b0..a07b5bf 100644 --- a/catbird/syntax.py +++ b/catbird/syntax.py @@ -451,12 +451,11 @@ def get_params_list(json_obj,syntax_path): attr_types = tuple(type_mapping[t] for t in param_info['basic_type'].split(':')) attr_type = attr_types[-1] + is_array=False if len(attr_types) > 1: for t in attr_types[:-1]: assert issubclass(t, Iterable) - ndim = len(attr_types) - 1 - else: - ndim = 0 + is_array=True # Set allowed values if present allowed_values = None @@ -469,22 +468,25 @@ def get_params_list(json_obj,syntax_path): # properties in the type definition as they are now default = None if 'default' in param_info.keys() and param_info['default'] != None and param_info['default'] != '': - if ndim > 0 and attr_type != str : + if is_array: defaults = param_info['default'] if type(defaults) == str: default = [_convert_to_type(attr_type, v) for v in defaults.split()] - else: + elif issubclass(type(defaults), Iterable): default = [_convert_to_type(attr_type, v) for v in defaults] + else: + default = [defaults] else: default = _convert_to_type(attr_type, param_info['default']) # Create and add a MOOSE parameter moose_param=MooseParam(param_name, attr_type, - description=param_info.get('description'), + is_array, default=default, - dim=ndim, - allowed_vals=allowed_values) + allowed_vals=allowed_values, + description=param_info.get('description'), + ) moose_param_list.append(moose_param) From 774c5bfec1f4ebfdd2c3a897e9c8ae451caaa287 Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Fri, 9 Feb 2024 10:09:37 +0000 Subject: [PATCH 55/62] Fix boilerplate mixin list for objects to be added to collections --- catbird/factory.py | 18 +++++++++++------- catbird/model.py | 9 +++++++-- catbird/syntax.py | 20 +++++++++++++++++--- 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/catbird/factory.py b/catbird/factory.py index 7dc7acd..71889c8 100644 --- a/catbird/factory.py +++ b/catbird/factory.py @@ -34,6 +34,8 @@ def _load_root_syntax(self,block_name, block): """ assert not block.is_leaf self.root_syntax[block.longname]=block.get_mixins() + self.collection_syntax[block.longname]=block.get_collection_mixins() + def _load_leaf_syntax(self,block_name,block,json_obj): """ @@ -66,10 +68,10 @@ def _load_leaf_syntax(self,block_name,block,json_obj): # Save class constructor self.constructors[parent_name][relation][lookup_name]=new_class - def load_enabled_objects(self,json_obj): self.constructors={} self.root_syntax={} + self.collection_syntax={} # Loop over enabled syntax blocks for block_name, block in self.available_blocks.items(): @@ -150,7 +152,7 @@ def __get_docstring(mixins): doc_now=doc_now+base.__doc__ return doc_now - def derive_class(self,root_name,obj_types,class_name): + def derive_class(self,root_name,obj_types,class_name,in_collection=False): """ Form a new mix-in class from a tuple of classes @@ -160,7 +162,10 @@ def derive_class(self,root_name,obj_types,class_name): obj_types: dict """ # Get mixins boilerplate - mixins=self.root_syntax[root_name] + if in_collection: + mixins=self.collection_syntax[root_name] + else: + mixins=self.root_syntax[root_name] # Update mixins list by comparing types mixins_now=deepcopy(mixins) @@ -213,8 +218,8 @@ def construct_root(self,root_name,obj_types,kwargs): return obj - def construct(self,root_name,obj_types,class_name,**kwargs): - class_now=self.derive_class(root_name,obj_types,class_name) + def construct(self,root_name,obj_types,class_name,in_collection,**kwargs): + class_now=self.derive_class(root_name,obj_types,class_name,in_collection) obj=class_now() # Handle keyword arguments @@ -296,5 +301,4 @@ def set_defaults(self): self.enable_syntax("BCs") self.enable_syntax("Materials") self.enable_syntax("VectorPostprocessors") - - #self.enable_syntax("Outputs") + self.enable_syntax("Outputs") diff --git a/catbird/model.py b/catbird/model.py index 92a145d..4f01b58 100644 --- a/catbird/model.py +++ b/catbird/model.py @@ -104,7 +104,7 @@ def add_to_collection(self, collection_name, class_name, object_name,**kwargs_in raise RuntimeError(msg) long_class_name=collection_name+"."+class_name - obj=self.factory.construct(collection_name,obj_types,long_class_name,**kwargs) + obj=self.factory.construct(collection_name,obj_types,long_class_name,in_collection=True,**kwargs) # Fetch collection and add collection = getattr(self, collection_name.lower()) @@ -133,6 +133,11 @@ def add_material(self,mat_name,mat_type,**kwargs_in): collection_kwargs["collection_type"]=mat_type self.add_to_collection("Materials","Material",mat_name,**collection_kwargs) + def add_output(self,output_name,output_type,**kwargs_in): + collection_kwargs=deepcopy(kwargs_in) + collection_kwargs["collection_type"]=output_type + self.add_to_collection("Outputs","Output",output_name,**collection_kwargs) + def to_str(self,print_default=False): model_str="" for obj_type in self.moose_objects: @@ -158,4 +163,4 @@ def load_default_syntax(self): self.add_syntax("Kernels") self.add_syntax("BCs") self.add_syntax("Materials") - #self.add_syntax("Outputs") + self.add_syntax("Outputs", action="CommonOutputAction") diff --git a/catbird/syntax.py b/catbird/syntax.py index a07b5bf..0e1667d 100644 --- a/catbird/syntax.py +++ b/catbird/syntax.py @@ -30,7 +30,7 @@ "action": MooseAction, "system": MooseCollection, "collection_type" : MooseCollection, - "collection_action": MooseCollection, # Don't support this syntax yet + "collection_action": MooseCollection, "nested_system": None, # The attribute should be added one layer down "nested_collection_action": None, # Don't support this syntax yet "nested_collection_type": None, # Don't support this syntax yet @@ -41,12 +41,18 @@ "action": MooseAction, "system": None, "collection_type" : MooseObject, - "collection_action": MooseAction, # Don't support this syntax yet - "nested_system": None, # The attribute should be added one layer down + "collection_action": MooseAction, + "nested_system": None, # Don't support this syntax yet "nested_collection_action": None, # Don't support this syntax yet "nested_collection_type": None, # Don't support this syntax yet } +_collection_type_map={ + "collection_type" : MooseObject, + "collection_action": MooseAction, +} + + def get_relation_kwargs(): return _relation_shorthands.values() @@ -219,6 +225,14 @@ def get_mixins(self): mixin_dict[relation_type]=mixin_now return mixin_dict + def get_collection_mixins(self): + mixin_dict={} + for relation_type in self.available_syntax.keys(): + if relation_type in _collection_type_map.keys(): + mixin_now=_collection_type_map[relation_type] + if mixin_now is not None: + mixin_dict[relation_type]=mixin_now + return mixin_dict @property def parent_longname(self): From 7811146f501688c7918e0b53ad860480b4b5265c Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Fri, 9 Feb 2024 10:38:30 +0000 Subject: [PATCH 56/62] add support for mesh generators --- catbird/factory.py | 6 +----- catbird/model.py | 7 ++++++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/catbird/factory.py b/catbird/factory.py index 71889c8..0f293fe 100644 --- a/catbird/factory.py +++ b/catbird/factory.py @@ -286,14 +286,10 @@ def load_config(self,filename): self.available_blocks[block_name].enabled=block_dict["enabled"] def set_defaults(self): - mesh_enable_dict={ - "obj_type": ["FileMesh","GeneratedMesh"] - } - self.enable_syntax("Mesh",enable_dict=mesh_enable_dict) - executioner_enable_dict={ "obj_type": ["Steady","Transient"] } + self.enable_syntax("Mesh") self.enable_syntax("Executioner", executioner_enable_dict) self.enable_syntax("Problem") self.enable_syntax("Variables") diff --git a/catbird/model.py b/catbird/model.py index 4f01b58..adc400d 100644 --- a/catbird/model.py +++ b/catbird/model.py @@ -138,6 +138,11 @@ def add_output(self,output_name,output_type,**kwargs_in): collection_kwargs["collection_type"]=output_type self.add_to_collection("Outputs","Output",output_name,**collection_kwargs) + def add_mesh_generator(self,mesh_generator_name,mesh_generator_type,**kwargs_in): + collection_kwargs=deepcopy(kwargs_in) + collection_kwargs["collection_type"]=mesh_generator_type + self.add_to_collection("Mesh","MeshGenerator",mesh_generator_name,**collection_kwargs) + def to_str(self,print_default=False): model_str="" for obj_type in self.moose_objects: @@ -158,7 +163,7 @@ def __init__(self,factory_in): def load_default_syntax(self): self.add_syntax("Executioner", obj_type="Transient") - self.add_syntax("Mesh", obj_type="GeneratedMesh") + self.add_syntax("Mesh") self.add_syntax("Variables") self.add_syntax("Kernels") self.add_syntax("BCs") From dd4471ccaaf2184b6ee47df030a015cc21bc884d Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Fri, 9 Feb 2024 13:57:53 +0000 Subject: [PATCH 57/62] Add a working thermal example --- examples/thermal.py | 103 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 examples/thermal.py diff --git a/examples/thermal.py b/examples/thermal.py new file mode 100644 index 0000000..204ae41 --- /dev/null +++ b/examples/thermal.py @@ -0,0 +1,103 @@ +from catbird import * +import os +import subprocess +import sys + +def main(): + # Get path to MOOSE + moose_path=os.environ['MOOSE_DIR'] + + # Path to executable and inputs + # TODO - update module name + module_name="heat_conduction" + app_name=module_name+"-opt" + app_path=os.path.join(moose_path,"modules",module_name) + app_exe=os.path.join(app_path,app_name) + + # Create a factory of available objects from our MOOSE executable + factory=Factory(app_exe) + + config_name="config_heat_conduction.json" + factory.write_config(config_name) + + # Create a boiler plate MOOSE model from a template + model=TransientModel(factory) + + # Set executioner attributes + model.executioner.end_time=5 + model.executioner.dt=1 # Default + + # Add a mesh generator + model.add_mesh_generator("generated","GeneratedMeshGenerator", + dim=2, + nx=10, + ny=10, + xmax=2, + ymax=1) + + # Add variables + var_name="T" + model.add_variable(var_name, initial_condition=300.0) + + # Add kernels + model.add_kernel("heat_conduction", kernel_type="HeatConduction", variable=var_name) + model.add_kernel("time_derivative", kernel_type="HeatConductionTimeDerivative", variable=var_name) + + # Add boundary conditions + model.add_bc("t_left", + bc_type="DirichletBC", + variable=var_name, + value = 300, + boundary='left') + + model.add_bc("t_right", + bc_type="FunctionDirichletBC", + variable=var_name, + function = "'300+5*t'", + boundary = 'right') + + # Add materials + model.add_material("thermal", + mat_type="HeatConductionMaterial", + thermal_conductivity=45.0, + specific_heat=0.5) + + model.add_material("density", + mat_type="GenericConstantMaterial", + prop_names='density', + prop_values=8000.0) + + model.outputs.exodus=True + model.add_output("csv",output_type="CSV", + file_base='therm_step03_out', + execute_on='final') + + # Add some input syntax that wasn't in the vanilla boilerplate model + model.add_syntax("VectorPostprocessors") + model.add_to_collection("VectorPostprocessors", + "VectorPostprocessor", + "t_sampler", + collection_type="LineValueSampler", + variable=var_name, + start_point='0 0.5 0', + end_point='2 0.5 0', + num_points=20, + sort_by='x') + + # Write out our input file + input_name="thermal.i" + model.write(input_name) + + # Run + args=[app_exe,'-i',input_name] + moose_process=subprocess.Popen(args) + stream_data=moose_process.communicate()[0] + retcode=moose_process.returncode + + # Return moose return code + sys.exit(retcode) + +if __name__ == "__main__": + main() + + From a1c16d37e245500d28f8b06512ff24794ee97759 Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Fri, 9 Feb 2024 15:28:56 +0000 Subject: [PATCH 58/62] refuse to print duplicated params, and enable print_defaults during file write --- catbird/factory.py | 11 +++++++++-- catbird/model.py | 4 ++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/catbird/factory.py b/catbird/factory.py index 0f293fe..9fb3adf 100644 --- a/catbird/factory.py +++ b/catbird/factory.py @@ -100,14 +100,21 @@ def __init__(self, *args, **kwargs): moose_param_dict_local=getattr(self,base.params_name) # Loop over and make into properties + duplicated_params=[] for attr_name, moose_param in moose_param_dict_local.items(): # Crucially, acts on the instance, not the class. if hasattr(self,attr_name): + # MOOSE is stupid, sometimes parameters can come from more than one source. + # If this happens, warn and skip, then remove from list afterwards + duplicated_params.append(attr_name) msg="Warning! Syntax collision for attribute {} in class {}. Skipping.".format(attr_name, self.__class__.__name__) print(msg) continue setattr(self,attr_name,moose_param.val) - #Todo: apply kwargs + for attr_name in duplicated_params: + moose_param_dict_local.pop(attr_name) + + #Todo: apply kwargs here? return __init__ @@ -117,7 +124,7 @@ def __get_inner_to_str_method(mixins): def inner_to_str(self,print_default=False): inner_str="" for base in mixins: - inner_str+=base.inner_to_str(self) + inner_str+=base.inner_to_str(self, print_default) return inner_str return inner_to_str diff --git a/catbird/model.py b/catbird/model.py index adc400d..12c5595 100644 --- a/catbird/model.py +++ b/catbird/model.py @@ -150,9 +150,9 @@ def to_str(self,print_default=False): model_str+=obj.to_str(print_default) return model_str - def write(self, filename): + def write(self, filename, print_default=False): file_handle = open(filename,'w') - file_handle.write(self.to_str()) + file_handle.write(self.to_str(print_default)) file_handle.close() print("Wrote to ",filename) From 30968bdf1e1e2264a7ac9d2f1aefc82dbb8f8956 Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Fri, 9 Feb 2024 15:29:44 +0000 Subject: [PATCH 59/62] Update README --- README.md | 127 ++++++++++++++++++++++++++++---------------- examples/thermal.py | 2 +- 2 files changed, 81 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 8f54ca5..f8eb821 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,6 @@ Catbird A code for generation of Python objects and input for various [MOOSE](https://moose.inl.gov/SitePages/Home.aspx). -*Caveats: This currently only parses data for the `MOOSE::Problem` type.* - Prerequisites ------------- @@ -29,63 +27,98 @@ To include packages for testsing, run pip install .[test] ``` -Example -------- +# Usage -Below is an example generating Python classes for the `OpenMCCellAverageProblem` and `NekRSProblem` types in the [Cardinal](https://cardinal.cels.anl.gov/). +## Load syntax into a Factory +First, import the package and load available syntax from a MOOSE application. ```python -In [1]: from catbird import app_from_exec +>>> from catbird import * +>>> factory=Factory('./heat_conduction-opt') +``` +This may take a few minutes, don't panic. The syntax first has to be dumped from the original application as json and parsed; +the factory then converts "available" syntax into python object constructors. The more syntax we enable the longer it takes, so if you want +to speed things up you only want to enable the syntax you intend to use. To write out what obejcts are currently enabled, try +```python +>>> factory.write_config("config.json") +``` +We could then edit that file and load it via an optional constructor argument to `Factory`: +```python +>>> lightweight_factory=factory=Factory('./heat_conduction-opt', config="lighweight_config.json") +``` +However, modifying the config file directory is likely to very laborious if done manually. Another option +is to derive your own Factory class and override the `set_defaults` method. To limit the enabled syntax, we can pass in dictionaries +of enabled syntax into the `enable_syntax` method, e.g.: +``` +executioner_enable_dict={ + "obj_type": ["Steady","Transient"] +} +self.enable_syntax("Mesh") +self.enable_syntax("Executioner", executioner_enable_dict) +``` -In [2]: cardinal = app_from_exec('./cardinal-opt') +## Create a Model +With our factory built, all we have in practice is bunch of constructor objects. We now need to create the objects and assemble as a `Model`: +```python +>>> model=MooseModel(factory) +``` +This will have created a "boiler-plate" with some sensible base objects to work with. We can now start to set their attributes in an object-oriented way. +``` +>>> model.mesh.dim=2 +``` +To see the available attributes for a given object, just use "help" to obtain useful documentation e.g.: +``` +>>> help(model.mesh) +``` +The help will also print the type and valid options. Type checking of attributes is performed, and if the type (i) incompatible with the expected type +and (ii) not castable as the expected type then a ValueError exception is raised, e.g. +``` +>>> model.mesh.dim='2' # This is fine, MOOSE expects string +>>> model.mesh.dim=2 # This is fine, str(2) is valid +>>> model.mesh.nx=1 # This is fine, MOOSE expects in +>>> model.mesh.nx="hello" # This raises a ValueError as int("hello") is invalid +``` -In [3]: openmc_prob = cardinal['problems']['OpenMCCellAverageProblem']() +Many of the objects are `Collection` types, i.e. a collection MOOSE objects, for example Variables. To add a variable, we must call: +``` +>>> model.add_variable("T", order="SECOND") +``` +Notice the use of key-word arguments in this function call. We can also act on the created object directly, via +``` +>>> model.add_variable("T") +>>> model.variables.objects["T"].order="SECOND" +``` -In [4]: openmc_prob.batches? -Type: property -String form: -Docstring: -Type: int -Number of batches to run in OpenMC; this overrides the setting in the XML files. +Even if the root-level syntax hasn't been added to the model (but is available in the factory) we can still add it to our model through `add_syntax` calls: +``` +model.add_syntax("VectorPostprocessors") +``` +Since this is a Collection type, to add a specific VectorPostprocessor, we call the add_to_collection method: +``` +model.add_to_collection("VectorPostprocessors","VectorPostprocessor","t_sampler") ``` -Features --------- +It is generally expected that the user will develop their own class derived from `MooseModel`, overriding the method `load_default_syntax`. +See for example TransientModel in `model.py`. -Every attribute comes with type and dimensionality checking (for array types). -This provides live feedback to users when setting problem parameters. In the example above, -setting `openmc_prob.batches` to a non-integer value results in the following +## Write to file +Finally if we are happy with our model we can write to file: ```python -In [5]: openmc_prob.batches = 100 - -In [6]: openmc_prob.batches = 'one hundred' ---------------------------------------------------------------------------- -ValueError Traceback (most recent call last) -Cell In[6], line 1 -----> 1 openmc_prob.batches = 'one hundred' - -File ~/soft/catbird/catbird/cbird.py:66, in Catbird.prop_set..fset(self, val) - 64 def fset(self, val): - 65 if dim == 0: ----> 66 self.check_type(name, val, attr_type) - 67 if allowed_vals is not None: - 68 self.check_vals(name, val, allowed_vals) - -File ~/soft/catbird/catbird/cbird.py:40, in Catbird.check_type(name, val, attr_type) - 38 val_type_str = val.__class__.__name__ - 39 exp_type_str = attr_type.__name__ ----> 40 raise ValueError(f'Incorrect type "{val_type_str}" for attribute "{name}". ' - 41 f'Expected type "{exp_type_str}".') - 42 return val +>>> model.write("catbird_input.i") +``` +You can run that input from the command line as you normally would or launch a subprocess. -ValueError: Incorrect type "str" for attribute "batches". Expected type "int". +Note, if you inspect our file `catbird_input.i` you may not see all attributes written out. The default behaviour is that parameters that are unchanged relative to +their default value are suppressed when writing to file. To see all attributes, instead run +```python +>>> model.write("catbird_input_full.i", print_default=True) ``` -Default values are also set on attributes automatically, so full problem -descriptions can be generated by setting only required parameters. +Example +------- +A fully worked heat conduction example may be found in: `catbird/examples/thermal.py`. Run with +```bash +$ python examples/thermal.py +``` -```python -In [7]: openmc_prob.initial_properties -Out[7]: 'moose' -``` \ No newline at end of file diff --git a/examples/thermal.py b/examples/thermal.py index 204ae41..b6c97be 100644 --- a/examples/thermal.py +++ b/examples/thermal.py @@ -69,7 +69,7 @@ def main(): model.outputs.exodus=True model.add_output("csv",output_type="CSV", - file_base='therm_step03_out', + file_base='thermal_out', execute_on='final') # Add some input syntax that wasn't in the vanilla boilerplate model From 9c891a63a6dcc0e55006e972d35c9b31fa70121b Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Fri, 9 Feb 2024 16:15:27 +0000 Subject: [PATCH 60/62] tweaks to README --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f8eb821..d662b6a 100644 --- a/README.md +++ b/README.md @@ -36,9 +36,9 @@ First, import the package and load available syntax from a MOOSE application. >>> from catbird import * >>> factory=Factory('./heat_conduction-opt') ``` -This may take a few minutes, don't panic. The syntax first has to be dumped from the original application as json and parsed; +This may take a few minutes, so don't panic if it hangs. The syntax first has to be dumped from the original application as json and then parsed; the factory then converts "available" syntax into python object constructors. The more syntax we enable the longer it takes, so if you want -to speed things up you only want to enable the syntax you intend to use. To write out what obejcts are currently enabled, try +to speed things up you should only enable the syntax you intend to use. To write out what objects are currently enabled, try: ```python >>> factory.write_config("config.json") ``` @@ -46,7 +46,7 @@ We could then edit that file and load it via an optional constructor argument to ```python >>> lightweight_factory=factory=Factory('./heat_conduction-opt', config="lighweight_config.json") ``` -However, modifying the config file directory is likely to very laborious if done manually. Another option +However, modifying the config file directory is likely to be very laborious if done manually. Another option is to derive your own Factory class and override the `set_defaults` method. To limit the enabled syntax, we can pass in dictionaries of enabled syntax into the `enable_syntax` method, e.g.: ``` @@ -58,11 +58,11 @@ self.enable_syntax("Executioner", executioner_enable_dict) ``` ## Create a Model -With our factory built, all we have in practice is bunch of constructor objects. We now need to create the objects and assemble as a `Model`: +With our factory built, all we have in practice is bunch of constructor objects. We now need to create the objects and assemble them as a `Model`: ```python >>> model=MooseModel(factory) ``` -This will have created a "boiler-plate" with some sensible base objects to work with. We can now start to set their attributes in an object-oriented way. +This will have created a "boiler-plate" model with some sensible base objects to work with. We can now start to set their attributes in an object-oriented way. ``` >>> model.mesh.dim=2 ``` From f8ddfaec524f6fb935c704c7176280ebe4e6475b Mon Sep 17 00:00:00 2001 From: helen-brooks <67463845+helen-brooks@users.noreply.github.com> Date: Tue, 27 Feb 2024 10:05:15 +0000 Subject: [PATCH 61/62] add pshriwise suggestion to json reader from code review Co-authored-by: Patrick Shriwise --- catbird/utils.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/catbird/utils.py b/catbird/utils.py index 2d684ad..d26c54d 100644 --- a/catbird/utils.py +++ b/catbird/utils.py @@ -22,15 +22,13 @@ def json_from_exec(exec): json_str = '' # filter out the header and footer from the json data + while json_proc.stdout.readline().decode().find('**START JSON DATA**') < 0: + pass + while True: line = json_proc.stdout.readline().decode() - if not line: + if not line or line.find('**END JSON DATA**') >= 0: break - if '**START JSON DATA**' in line: - continue - if '**END JSON DATA**' in line: - continue - json_str += line j_obj = json.loads(json_str) From dc92148f35e6729006eaf95626bc8e85584b0d59 Mon Sep 17 00:00:00 2001 From: Helen Brooks Date: Tue, 27 Feb 2024 10:16:29 +0000 Subject: [PATCH 62/62] update thermal example to use up-to-date module name --- examples/thermal.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/thermal.py b/examples/thermal.py index b6c97be..1734413 100644 --- a/examples/thermal.py +++ b/examples/thermal.py @@ -8,8 +8,7 @@ def main(): moose_path=os.environ['MOOSE_DIR'] # Path to executable and inputs - # TODO - update module name - module_name="heat_conduction" + module_name="heat_transfer" app_name=module_name+"-opt" app_path=os.path.join(moose_path,"modules",module_name) app_exe=os.path.join(app_path,app_name)