def set_component_state(self, component_name, state_id): """Change a component's state on the main graph.""" if component_name not in self.mapping: raise exceptions.BadInputError( f"Component '{component_name}' not found in mapping dict.") # Map from component to graph node for this component component_map = self.mapping[component_name] component = self.component_dict[component_name] if state_id not in component.states: raise exceptions.BadInputError( f"State '{state_id}' not found in {component_name} states dict." ) # Dict of {edges: FC} with component node names state_edges_component = component.states[state_id] component.current_state = state_id # Create new dict keyed by graph edges rather than component ones state_edges_graph = {} for cedge in state_edges_component.keys(): cstart_node, cend_node, key = cedge both_nodes_valid = True if cstart_node not in component_map: error = invalid.InvalidComponentNode( f"Component '{component.name}', node {cstart_node} not found in mapping dict.", component.name, cstart_node) invalid.add_error(error, self.error_set) both_nodes_valid = False if cend_node not in component_map: error = invalid.InvalidComponentNode( f"Component '{component.name}', node {cend_node} not found in mapping dict.", component.name, cend_node) invalid.add_error(error, self.error_set) both_nodes_valid = False if both_nodes_valid: new_edge = (component_map[cstart_node], component_map[cend_node], component_name + '.' + key) state_edges_graph[new_edge] = state_edges_component[cedge] # Set FC on main graph according to new dict nx.classes.function.set_edge_attributes(self.plumbing_graph, state_edges_graph, 'FC')
def current_FC(self, *args): """Given a component_name or edge_id, return a dict of corresponding FCs. Passing in a component_name will yield a dict of all associated edges and FCs, while passing in a single edge_ID will simply yield a single value. Accepts lists, arguments, or some combination thereof, but **not tuples**. """ if len(args) == 0: return { edge: self.plumbing_graph.edges[edge]['FC'] for edge in self.plumbing_graph.edges(keys=True) } # If passed a list, unpack those list elements into args args = utils.flatten(args, unpack_tuples=False) if len(args) == 1 and args[0] in list( self.plumbing_graph.edges(keys=True)): return self.plumbing_graph.edges[args[0]]['FC'] ret = {} for arg in args: if arg in self.component_dict: for edge in self.plumbing_graph.edges(keys=True): if edge[2].startswith(arg + '.'): ret[edge] = self.plumbing_graph.edges[edge]['FC'] elif arg in list(self.plumbing_graph.edges(keys=True)): ret[arg] = self.plumbing_graph.edges[arg]['FC'] else: raise exceptions.BadInputError( f"'{arg}' not found as component name or edge identifier.") return ret
def current_pressures(self, *args): """Given one or more nodes, return their current pressure. Can accept lists, tuples, series of separate arguments, or any combination of the above. If given a single argument, returns a single value. Otherwise, returns a dict of {node: pressure}. """ if len(args) == 0: return { n: self.get_node_body(n).get_pressure() for n in self.plumbing_graph.nodes() } # If passed a list, unpack those list elements into args args = utils.flatten(args) try: if len(args) == 1: return self.get_node_body(args[0]).get_pressure() else: return {n: self.get_node_body(n).get_pressure() for n in args} except KeyError as err: raise exceptions.BadInputError( f"Node {err.args[0]} not found in graph.")
def current_state(self, *args): """Given one or more component_names, return the state_id of their current states. Can accept lists, tuples, series of separate arguments, or any combination of the above. If given a single argument, returns a single value. Otherwise, returns a dict of {component_name: state}. """ if len(args) == 0: return { component.name: component.current_state for component in self.component_dict.values() } # If passed a list, unpack those list elements into args args = utils.flatten(args) try: if len(args) == 1: component_name = args[0] return self.component_dict[component_name].current_state else: return { name: self.component_dict[name].current_state for name in args } except KeyError as err: raise exceptions.BadInputError( f"Component '{err.args[0]}' not found in component dict.")
def reverse_orientation(self, component_name): """Reverse direction of suitable components, such as check valves.""" if component_name not in self.component_dict: raise exceptions.BadInputError( f"Component '{component_name}' not found in component dict.") component = self.component_dict[component_name] if len(component.component_graph.edges()) != 2: raise exceptions.InvalidComponentError( "Component must only have two edges to be automatically reversed.\n" "Consider adjusting direction manually.") # Reverse orientation by switching direction of FCs to_switch = [ e for e in self.plumbing_graph.edges(keys=True) if component_name in e[2] ] edge1 = list(to_switch[0]) edge2 = list(to_switch[1]) temp = self.plumbing_graph.edges[edge1]['FC'] self.plumbing_graph.edges[edge1]['FC'] = self.plumbing_graph.edges[ edge2]['FC'] self.plumbing_graph.edges[edge2]['FC'] = temp
def remove_component(self, input_component_name): """Remove component and associated errors.""" # Check validity of provided component name if input_component_name not in self.component_dict: raise exceptions.BadInputError( f"Component with name {input_component_name} not found in component dict." ) component = self.component_dict[input_component_name] component_name = component.name # Remove all edges associated with component to_remove = [] for edge in self.plumbing_graph.edges(keys=True): if component_name in edge[2]: to_remove.append(edge) self.plumbing_graph.remove_edges_from(to_remove) # Remove unconnected (redundant) nodes to_remove = [] for node in self.plumbing_graph.nodes(): if not list(self.plumbing_graph.neighbors(node)): to_remove.append(node) self.plumbing_graph.remove_nodes_from(to_remove) # Self info housekeeping self._resolve_errors(input_component_name) if component_name in self.mapping: del self.mapping[component_name] del self.component_dict[input_component_name] self.time_res = utils.DEFAULT_TIME_RESOLUTION_MICROS for name in self.component_dict.keys(): self._set_time_res(name)
def set_teq(self, component_name, which_edge): """Set teq at each edge in provided dict for one component. which_edge is a dict of {edge: teq}. edge is the standard tuple of the form (source, target, key), where source and target are nodes, and key is a unique identifier. """ if component_name not in self.component_dict: raise exceptions.BadInputError( f"Component name '{component_name}' not found in component dict." ) component = self.component_dict[component_name] which_edge = copy.deepcopy(which_edge) for state_id, edge_dict in which_edge.items(): if state_id not in component.states: raise exceptions.BadInputError( f"State '{state_id}' not found in component {component_name}'s states dict." ) for edge, teq in edge_dict.items(): teq = utils.s_to_micros(teq) if teq < utils.TEQ_MIN: raise exceptions.BadInputError( f"Provided teq {utils.micros_to_s(teq)} (component '{component_name}'," f" state '{state_id}', edge {edge}) too low. " f"Minimum teq is {utils.micros_to_s(utils.TEQ_MIN)}s.") if edge not in component.states[state_id]: raise exceptions.BadInputError( f"State '{state_id}', edge {edge} not found in component" f" {component_name}'s states dict.") component.states[state_id][edge] = utils.teq_to_FC(teq) # Update teq changes on main plumbing graph if component.current_state in which_edge.keys(): self.set_component_state(component_name, component.current_state) self._set_time_res(component_name)
def set_pressure(self, node_name, pressure, fixed=False): """Set pressure at given node.""" if not isinstance(pressure, (int, float)): raise exceptions.BadInputError( f"Pressure {pressure} must be a number.") if pressure < 0: raise exceptions.BadInputError( f"Negative pressure {pressure} not allowed.") if node_name not in self.plumbing_graph: raise exceptions.BadInputError( f"Node {node_name} not found in graph.") if node_name == utils.ATM and pressure != 0: raise exceptions.BadInputError( f"Pressure for atmosphere node ({utils.ATM}) must be 0.") self.get_node_body(node_name).update_pressure(pressure) self.get_node_body(node_name).update_fixed(fixed) if fixed: self.fixed_pressures[node_name] = pressure if not fixed and node_name in self.fixed_pressures: del self.fixed_pressures[node_name]
def step(self, timestep=None): """ Return node pressures in the engine after timestep has elapsed. Step cannot be called on an empty or invalid plumbing engine. Parameters ---------- timestep: int timestep is the time, in microseconds, that we allow to elapse before returning the new state of node pressures in the graph. If unspecified, it defaults to the engine's current automatic time_res. If timestep is lower than the current time_res, time_res will be set to timestep and timestep will be used for calculations; however timestep must still be greater than MIN_TIME_RES. If not, an error will be raised. Returns a dict of {node: pressure}, much like current_pressures(). """ if not self.plumbing_graph: raise exceptions.InvalidEngineError( "Step() cannot be called on an empty engine.") if not self.is_valid(): raise exceptions.InvalidEngineError( "Step() cannot be called on an invalid engine. Check for errors." ) if timestep is None: timestep = self.time_res if timestep < utils.MIN_TIME_RES_MICROS: raise exceptions.BadInputError( f"timestep ({timestep}) too low, must be greater than " f"{utils.MIN_TIME_RES_MICROS} us.") if timestep < self.time_res: self.time_res = timestep if int(timestep) != timestep: raise exceptions.BadInputError( f"timestep ({timestep}) must be integer.") new_pressures = {} max_time = self.time + timestep while self.time < max_time: time_res = self.time_res if self.time + self.time_res > max_time: time_res = max_time - self.time for node, data in self.nodes(): if node in self.fixed_pressures or node == utils.ATM: continue dp = 0 pressure = data['body'].get_pressure() for edge in self.plumbing_graph.out_edges(node, keys=True): neighbor = edge[1] npressure = self.current_pressures(neighbor) if pressure > npressure: fc = self.current_FC(edge) dp -= fc * (pressure - npressure) for edge in self.plumbing_graph.in_edges(node, keys=True): neighbor = edge[0] npressure = self.current_pressures(neighbor) if pressure < npressure: fc = self.current_FC(edge) dp += fc * (npressure - pressure) new_pressures[node] = pressure + dp * time_res for node, pressure in new_pressures.items(): self.set_pressure(node, pressure) self.time += time_res return self.current_pressures()
def add_component(self, component, mapping, state_id, pressures=None, fail_silently=False): """ Add a component to the main plumbing graph according to provided specifications. Specifications are similar to load_graph(), but localized to a single component. Parameters ---------- component: PlumbingComponent component is the PlumbingComponent to be added. mapping: dict mapping is a dict of {component_node: main_graph_node} that specifies connectivity between the added component and the rest of the graph. state_id: string state_id is the component's initial state. pressures: dict pressures is a dict of {main_graph_node: (initial_pressure, fixed)} where fixed is a bool indicating whether the pressure must remain fixed or not. The dict doesn't have to be exhaustive; if a node isn't specified its pressure will be set to a default of 0. fail_silently: bool fail_silently controls whether errors are raised or written to the error set. """ if not fail_silently and not component.is_valid(): raise exceptions.BadInputError( "Component not valid; all errors must be resolved before loading in." ) if not pressures: pressures = {} name = component.name component_graph = component.component_graph # Updating the plumbing engine's records about itself with new component self.component_dict[name] = component self.mapping[name] = copy.deepcopy(mapping) self._set_time_res(name) # Adding and connecting new nodes to main graph as necessary for start_node, end_node, edge_key in component_graph.edges(keys=True): both_nodes_valid = True if start_node not in mapping: error_msg = f"Component '{name}', node {start_node} not found in mapping dict." if fail_silently: error = invalid.InvalidComponentNode( error_msg, name, start_node) invalid.add_error(error, self.error_set) both_nodes_valid = False else: raise exceptions.BadInputError(error_msg) if end_node not in mapping: error_msg = f"Component '{name}', node {end_node} not found in mapping dict." if fail_silently: error = invalid.InvalidComponentNode( error_msg, name, end_node) invalid.add_error(error, self.error_set) both_nodes_valid = False else: raise exceptions.BadInputError(error_msg) if both_nodes_valid: start_map_node = mapping[start_node] end_map_node = mapping[end_node] self.plumbing_graph.add_edge(start_map_node, end_map_node, component.name + '.' + edge_key) for node in [start_map_node, end_map_node]: if node in self.nodes( data=False ) and 'body' in self.plumbing_graph.nodes[node]: continue body = node_types.instantiate_node(node) self.plumbing_graph.nodes[node]['body'] = body self.set_component_state(component.name, state_id) # Assign specified node pressures pressures = copy.deepcopy(pressures) for node_name, node_pressure in pressures.items(): try: self.set_pressure(node_name, node_pressure[0], fixed=node_pressure[1]) except exceptions.BadInputError as err: if fail_silently: if err.args[0] == f"Node {node_name} not found in graph.": raise error = invalid.InvalidNodePressure(err.args[0], node_name) invalid.add_error(error, self.error_set) else: raise
def load_graph(self, components, mapping, initial_pressures, initial_states): """ Load in a graph to the PlumbingEngine. Parameters ---------- components: dict components is a dict of {component_name: PlumbingComponent} where component_name is a string that matches the name attribute of its corresponding PlumbingComponent. mapping: dict mapping is a dict of {component_name: {component_node: main_graph_node}}, which is used to specify connectivity between components on the main graph. initial_pressures: dict initial_pressures is a dict of {main_graph_node: (initial_pressure, fixed)} where fixed is a bool indicating whether the pressure must remain fixed or not. The dict doesn't have to be exhaustive; if a node isn't specified its pressure will be set to a default of 0. initial_states: dict initial_states is a dict of {component_name: state_name}. Every component must have an entry, which will be used to determine its initial state on the main graph. Errors from malformed input will be stored in the engine's error_set. The presence of errors renders the engine invalid; invalid engines cannot be solved. """ initial_pressures = copy.deepcopy(initial_pressures) initial_states = copy.deepcopy(initial_states) self.component_dict = copy.deepcopy(components) self.mapping = copy.deepcopy(mapping) self.plumbing_graph.clear() self.error_set.clear() for name, component in self.component_dict.items(): if not component.is_valid(): error = invalid.InvalidComponentName( f"Component with name '{name}' is not valid;" " component cannot be loaded in until errors are resolved.", name) invalid.add_error(error, self.error_set) name_valid = True if name not in self.mapping: error = invalid.InvalidComponentName( f"Component with name '{name}' not found in mapping dict.", name) invalid.add_error(error, self.error_set) name_valid = False if name not in initial_states: error = invalid.InvalidComponentName( f"Component '{name}' state not found in initial states dict.", name) invalid.add_error(error, self.error_set) name_valid = False if not name_valid: continue # Only pass in those pressures that are relevant to the current component node_pressures = {} for node, pressure in initial_pressures.items(): if node in self.mapping[name].values(): node_pressures[node] = pressure self.add_component(component, mapping[name], initial_states[name], node_pressures, fail_silently=True) # Raise this error (instead of writing to the error set) because there's no intuitive # point to remove afterwards. Won't interfere with any engine setup, since it's at the very # end of the function call for node in initial_pressures.keys(): if node not in self.plumbing_graph.nodes(): raise exceptions.BadInputError( f"Node {node} not found in graph.")