def validate_type_entry(self, entry): """Validate typedef implementation components specifically.""" check_fields(entry, ['name', 'type', 'params']) def_type = entry['type'] # a '.' indicates it's an import, which will be checked later. if '.' in def_type: self.components.append(entry) return if def_type not in self.typedefs: raise exceptions.BadInputError( f"typedef {def_type} not found; typedef must " "be defined before being referenced") params = self.typedefs[def_type]['params'] if len(params) < len(entry['params']): raise exceptions.BadInputError( f"params ({entry['params']}) present in component " "declaration not found in typedef decleration") # default arguments using syntax (parameter=default_value) for param in params: if '=' in param: default_param = param.split('=') if not default_param[1]: raise exceptions.BadInputError( "default argument operator ('=') used for " f"param {param} but no argument found") elif param not in entry['params']: raise exceptions.BadInputError( f"param {param} not found and default argument " "not specified") self.components.append(entry)
def check_fields(entry, fields): """Check that all provided fields are in entry, where entry is a PDL object.""" if entry is None: raise exceptions.BadInputError("empty entry") for field in fields: if field not in entry: raise exceptions.BadInputError( f"field {field} required and not found") if entry[field] is None: # empty fields will be loaded in as None by pyyaml raise exceptions.BadInputError( f"field {field} required not to be empty")
def parse_graphs(self): """Extract and store graph information for plumbing engine.""" graphs = self.package.graphs() main_present = False # move main graph to end so that its settings take precedence for idx, entry in enumerate(graphs): if entry['name'] == 'main': main_present = True graphs.append(graphs.pop(idx)) break if not main_present: raise exceptions.BadInputError("must have graph main") for entry in graphs: for graph_node, node_data in entry['nodes'].items(): if 'initial_pressure' in node_data: self.initial_pressures[graph_node] = ( node_data['initial_pressure'], False) if 'fixed_pressure' in node_data: self.initial_pressures[graph_node] = ( node_data['fixed_pressure'], True) for component_name, component_node in node_data['components']: if component_name not in self.mapping: self.mapping[component_name] = {} self.mapping[component_name][component_node] = graph_node self.initial_states.update(entry['states'])
def fill_typedef(self, namespace, component): """Fill in typedef template for components invoking a typedef.""" name = component['type'] component_name = component['name'] if name.count('.') > 1: raise NotImplementedError( f"nested imports (in {name}) not supported yet") # handle imported components if '.' in name: # NOTE: we might eventually want to consider how well this will play with nested imports fields = name.split('.') namespace = fields[0] name = fields[-1] if name not in self.typedefs[namespace]: raise exceptions.BadInputError(f"invalid component type: {name}") # default arguments using syntax (parameter=default_value) for param in self.typedefs[namespace][name]['params']: if '=' in param: default_param = param.split('=') if default_param[0] not in component['params']: component['params'][default_param[0]] = default_param[1] params = component['params'] body = yaml.dump(self.typedefs[namespace][name]) for var, value in params.items(): body = body.replace(var, str(value)) ret = yaml.safe_load(body) ret.pop('params') ret['name'] = component_name return ret
def validate(self): """Validate the PDL contents of the File.""" for entry in self.body: e_type = list(entry.keys())[0] if e_type not in self.type_checks: raise exceptions.BadInputError(f"invalid input type {e_type}") body = entry[e_type] self.type_checks[e_type](body)
def validate_graph(self, entry): """Validate graph entries specifically.""" check_fields(entry, ['name', 'nodes']) for node_name in entry['nodes']: node = entry['nodes'][node_name] if len(node) < 1 or 'components' not in node: raise exceptions.BadInputError( f"invalid entry for {node_name}: {node}") self.graphs.append(entry)
def validate_component(self, entry): """Validate component entries specifically.""" if 'type' in entry: self.validate_type_entry(entry) return check_fields(entry, ['name', 'edges']) if 'states' not in entry: for edge in entry['edges']: edge_value = entry['edges'][edge] if 'nodes' not in edge_value or 'teq' not in edge_value or len( edge_value) != 2: raise exceptions.BadInputError( f'invalid single-state component syntax in {entry}') self.components.append(entry)
def extract_edges(entry): """ Extract dict of {edge_name: (fwd_edge, back_edge)} from a component entry. fwd_edge and back_edge take the form (node1, node2, key), where key is unique among edges going between the same nodes. """ name = entry['name'] edge_dict = {} # edges_seen keeps track of edges between the same nodes. Takes form # {(node1, node2): key}, where key is the lowest integer that has been used # as a key for this set of nodes. edges_seen = {} for edge_name, edges in entry['edges'].items(): if len(edges['nodes']) != 2: raise exceptions.BadInputError( f"malformed nodes entry ({edges['nodes']}) for edge {edge_name} in" + f" component {name}") # key will just be fwd or back, unless there are multiple edges between the same # two nodes, in which case the key will have a unique int appended. key = '' nodes = tuple(edges['nodes']) swapped_nodes = (nodes[1], nodes[0]) if nodes in edges_seen: edges_seen[nodes] += 1 key = edges_seen[nodes] elif swapped_nodes in edges_seen: edges_seen[swapped_nodes] += 1 key = edges_seen[swapped_nodes] else: edges_seen[nodes] = 1 node_1, node_2 = edges['nodes'] fwd_edge = (node_1, node_2, 'fwd' + str(key)) back_edge = (node_2, node_1, 'back' + str(key)) edge_dict[edge_name] = (fwd_edge, back_edge) return edge_dict
def fill_blank_states(self, graph, default_states): """Fill in states field with default states if left blank.""" if 'states' not in graph: graph['states'] = {} # set of components in this graph components = set() for node in graph['nodes'].values(): for component in node['components']: components.add(component[0]) for component in components: if component in graph['states']: continue if component not in default_states: raise exceptions.BadInputError( f"missing component {component}: either a nonexistent or a " "multi-state component") graph['states'][component] = default_states[component]
def __init__(self, path, input_type='f'): """ Initialize a File object from a file's worth of PDL. The PDL file should contain exactly three fields: - name: the namespace that the file's contents belong in, - imports: a list of imports that the file's contents use, - body: the body of PDL. Parameters ---------- path: string path should contain either the path to the file containing PDL, or a string that contains a valid PDL file by itself (mostly for testing purposes). input_type: char input_type indicates whether the argument provided to "path" is a file (f) path or a string (s). Instantiating a File automatically validates its contents; a successful initialization produces a ready-to-use File. Fields ------ imports: list list of imports (by name) that are relevant to this file. typedefs: dict dict of {typedef name: typedef body}, used to access typedef definitions by name. components: list list of PDL component bodies, stored as objects. graphs: list list of PDL graph bodies, stored as objects. """ if input_type == 'f': file = open(path, 'r') elif input_type == 's': file = path else: raise exceptions.BadInputError(f"invalid input type {input_type}") pdl = yaml.safe_load(file) self.type_checks = { 'typedef': self.validate_typedef, 'component': self.validate_component, 'graph': self.validate_graph, } self.imports = [] if 'import' in pdl: self.imports = pdl['import'] self.namespace = pdl['name'] self.body = pdl['body'] self.typedefs = {} self.components = [] self.graphs = [] self.validate()
def __init__(self, files, import_paths=None): """ Initialize a Package from one or more Files. A Package should have all the components of a complete plumbing engine system; from here no additional information will make it into the PlumbingEngine. Once instantiated, a Package's PDL is cleaned and ready to use. Parameters ---------- files: iterable files is an iterable (usually a list) of one or more Files whose contents should go into the Package. """ self.import_paths = copy.deepcopy(import_paths) if import_paths is None: self.import_paths = utils.default_paths self.importable_files = dict() imports_folder = [] for import_path in self.import_paths: try: filenames = os.listdir(import_path) filenames = [ os.path.join(import_path, fname) for fname in filenames ] imports_folder.extend(filenames) except FileNotFoundError: imports_folder = [] warnings.warn( f"import directory {import_path} could not be found") for path in imports_folder: try: name = yaml.safe_load(open(path, 'r'))['name'] if name in self.importable_files: self.importable_files[name].add(path) else: self.importable_files[name] = {path} except KeyError: warnings.warn(path + " does not describe a pdl file") if len(list(files)) < 1: raise exceptions.BadInputError( "cannot instantiate a Package with no Files") self.imports = [] # dicts of {namespace: [entries]}, where entry is a PDL object. Organized like this to # reduce dict nesting; since this is a one time process it should be easy to keep # them synced. self.typedefs = {} self.component_dict = {} self.graph_dict = {} for file in files: # TODO(wendi): unused import detection self.imports.extend(copy.deepcopy(file.imports)) for imp in set(self.imports): if imp not in self.importable_files: raise exceptions.BadInputError(f"invalid import: {imp}") for path in self.importable_files[imp]: files.append(top.File(path)) # consolidate entry information from files for file in files: name = file.namespace if name not in self.typedefs: self.typedefs[name] = {} self.component_dict[name] = [] self.graph_dict[name] = [] self.typedefs[name].update(copy.deepcopy(file.typedefs)) self.component_dict[name].extend(copy.deepcopy(file.components)) self.graph_dict[name].extend(copy.deepcopy(file.graphs)) self.clean()