def rebase_fragment(self, fragment, flow, direction): """ creates a new fragment by inverting an existing one (via traversal), specifying an alternate flow as the reference. basically wraps the fragment and sets the exchange value, then deletes one child flow. doesn't currently work if the fragment contains internal reference flow feedback :param fragment: :param exchange: should be output from F.get_fragment_inventory :return: """ comment = 'rebased %.5s' % fragment.get_uuid() exchange = next(ex for ex in self.get_fragment_inventory(fragment) if ex.flow is flow and ex.direction == direction) frag = self.new_fragment(exchange.flow, comp_dir(exchange.direction), Comment=comment, exchange_value=exchange.value) frag.terminate(frag) self.create_fragment(parent=frag, flow=fragment.flow, direction=comp_dir(exchange.direction), comment=comment, value=fragment.cached_ev) n = self.create_fragment(parent=frag, flow=fragment.flow, direction=fragment.direction, comment=comment, value=fragment.cached_ev) n.terminate(fragment) ch = self.build_child_flows(n, background_children=False) for c in ch: if c.flow is exchange.flow: c.term.self_terminate() return frag
def create_fragment_from_process(self, process_ref, ref_flow=None, background=False, background_children=True): """ The major entry into fragment building. Given only a process ref, construct a fragment from the process, using the process's reference exchange as the reference fragment flow. :param process_ref: :param ref_flow: :param background: [False] add as a background fragment and do not traverse children :param background_children: [True] automatically terminate child flows with background references (ecoinvent). :return: """ process = process_ref.fg() if ref_flow is None: if len(process.reference_entity) == 0: ref = pick_one([x for x in self.db.filter_exch(process_ref, elem=False) if x.direction == 'Output'] ).flow elif len(process.reference_entity) > 1: ref = pick_reference(process) else: ref = list(process.reference_entity)[0].flow else: try: ref = next(x.flow for x in process.reference_entity if x.flow.match(ref_flow)) except StopIteration: print('Reference flow not found in target process.') return None ref_exch = next(x for x in process.exchange(ref)) direction = comp_dir(ref_exch.direction) frag = self[0].create_fragment(ref, direction, Name='%s' % process_ref.entity(), background=background) frag.terminate(process_ref, flow=ref) if not background: self.build_child_flows(frag, background_children=background_children) return frag
def show_balance(self, frag, quantity=None, scenario=None, observed=False): def _p_line(f, m, d): try: # will fail if m is None or non-number print(' %+10.4g %6s %.5s %s' % (m, d, f.get_uuid(), f['Name'])) finally: pass if quantity is None: quantity = frag.flow.reference_entity print('%s' % quantity) mag = frag.flow.cf(quantity) if frag.reference_entity is None: mag *= frag.exchange_value(scenario, observed=observed) if frag.direction == 'Input': mag *= -1 net = mag _p_line(frag, mag, comp_dir(frag.direction)) for c in sorted(self.child_flows(frag), key=lambda x: x.direction): mag = c.exchange_value(scenario, observed=observed) * c.flow.cf(quantity) if c.direction == 'Output': mag *= -1 if mag is None or mag != 0: _p_line(c, mag, c.direction) net += mag print('----------\n %+10.4g net' % net)
def clone_fragment(self, frag, parent=None, suffix=' (copy)'): """ Creates duplicates of the fragment and its children. returns the reference fragment. :param frag: :param parent: used internally :param suffix: attached to top level fragment :return: """ if parent is None: parent = frag.reference_entity if parent is None: direction = comp_dir(frag.direction) # this gets re-reversed in create_fragment else: direction = frag.direction new = self.create_fragment(parent=parent, Name=frag['Name'] + suffix, StageName=frag['StageName'], flow=frag.flow, direction=direction, comment=frag['Comment'], value=frag.cached_ev, balance=frag._balance_flow) self.transfer_evs(frag, new) for t_scen in frag.terminations(): term = frag.termination(t_scen) if term.term_node is frag: self.terminate_to_foreground(new, scenario=t_scen) else: new.term_from_term(term, scenario=t_scen) for c in self.child_flows(frag): self.clone_fragment(c, parent=new, suffix='') return new
def create_fragment(self, parent=None, flow=None, direction=None, comment=None, value=None, balance=False, **kwargs): """ :param parent: :param flow: :param direction: :param comment: :param value: :param balance: :param kwargs: :return: """ if flow is None: ch = cyoa('(N)ew flow or (S)earch for flow? ', 'ns') if ch == 'n': flow = self.new_flow() elif ch == 's': index = self.current_archive or select_archive(self) elem = {'i': False, 'e': True, 'a': None}[cyoa('(I)ntermediate, (E)lementary, or (A)ll flows? ', 'aei', 'I').lower()] flow = self.find_flow(None, index=index, elementary=elem) if flow is None: return None flow = flow.entity() else: raise ValueError print('Creating fragment with flow %s' % flow) direction = direction or {'i': 'Input', 'o': 'Output'}[cyoa('flow is (I)nput or (O)utput?', 'IO').lower()] comment = comment or ifinput('Enter FragmentFlow comment: ', '') if parent is None: # direction reversed for UX! user inputs direction w.r.t. fragment, not w.r.t. parent if value is None: value = 1.0 frag = self.new_fragment(flow, comp_dir(direction), Comment=comment, exchange_value=value, **kwargs) else: if parent.term.is_null: self.terminate_to_foreground(parent) if value is None: val = ifinput('Exchange value (%s per %s): ' % (flow.unit(), parent.unit), '1.0') if val == '1.0': value = 1.0 else: value = parse_math(val) frag = self[0].add_child_fragment_flow(parent, flow, direction, Comment=comment, exchange_value=value, **kwargs) if balance: frag.set_balance_flow() self.traverse(parent) if self.db.is_elementary(frag.flow): self.terminate_to_foreground(frag) return frag
def __init__(self, fragment, process_ref, direction=None, term_flow=None, descend=True, inbound_ev=None): self._parent = fragment self._process_ref = process_ref # this is either a catalog_ref (for process) or just a fragment self._descend = True self.term_flow = None self._cached_ev = 1.0 self._score_cache = LciaResults(fragment) if direction is None: self.direction = comp_dir(fragment.direction) else: self.direction = direction self.descend = descend self.set_term_flow(term_flow) self._set_inbound_ev(inbound_ev) self.validate_flow_conversion()
def serialize(self): if self._process_ref is None: return {} if self.index == 0: source = 'foreground' else: source = self._process_ref.catalog.source_for_index(self._process_ref.index) j = { 'source': source, 'entityId': self.id } if self.term_flow != self._parent.flow: j['termFlow'] = self.term_flow.get_uuid() if self.direction != comp_dir(self._parent.direction): j['direction'] = self.direction if self._descend is False: j['descend'] = False # don't serialize score cache- could, of course return j
def terminates(self, exchange): """ Returns True if the exchange's termination matches the term's term_node, and the flows also match, and the directions are complementary. If the exchange does not specify a termination, returns True if the flows match and directions are comp. :param exchange: :return: """ if self.term_flow.match(exchange.flow) and self.direction == comp_dir(exchange.direction): if exchange.termination is None: return True else: if self.is_null: return False if self.term_node.entity_type != 'process': return False if exchange.termination == self._process_ref.id: return True return False
def revise_exchanges(self, frag, scenario=None): """ interactively update reference exchange values (or values for a particular scenario) for the children of a given fragment :param frag: :param scenario: :return: """ print('Reference flow: [%s] %s' % (comp_dir(frag.direction), frag.unit)) if scenario is None: print('Update reference flow') else: print('Update reference flow for scenario "%s"' % scenario) self._update_ev(frag, scenario) for c in self.child_flows(frag): print(' Child flow: %s ' % c) if scenario is None: print('Update default value') else: print('Update value for scenario "%s"' % scenario) self._update_ev(c, scenario)
def traverse(self, childflows, upstream_nw, scenario, observed=False, frags_seen=None, conserved_qty=None, _balance=None): """ If the node has a non-null termination, use that; follow child flows. If the node's termination is null- then look for matching background fragments. If one is found, adopt its termination, and return. else: assume it is a null foreground node; follow child flows :param childflows: this is a lambda that takes current frag and returns a generator of frags listing self as parent - must be provided by calling environment :param upstream_nw: upstream node weight :param scenario: string or tuple of strings :param observed: whether to use observed or cached evs (overridden by scenario specification) :param frags_seen: carried along to catch recursion loops :param conserved_qty: in case the parent node is a conservation node :param _balance: used when flow magnitude is determined during traversal, i.e. for balance flows and children of fragment nodes :return: an array of FragmentFlow records reporting the traversal """ def _print(qwer, level=1): self._print(qwer, level=level) ''' First handle the traversal entry inputs: _balance conserved_qty observed scenario upstream_nw outputs: own ff conserved_val ''' if _balance is None: if self.reference_entity is None: # reference fragment exchange values are inbound ev = 1.0 / self.exchange_value(scenario, observed=observed) else: ev = self.exchange_value(scenario, observed=observed) else: _print('%.3s %g balance' % (self.get_uuid(), _balance), level=2) ev = _balance self._cache_balance_ev(_balance, scenario) magnitude = upstream_nw * ev conserved_val = None conserved = False if conserved_qty is not None: if self._balance_flow: raise BalanceFlowError # to be caught conserved_val = ev * self.flow.cf(conserved_qty) if conserved_val != 0: conserved = True if self.direction == 'Output': # convention: inputs to parent are positive conserved_val *= -1 _print('%.3s %g' % (self.get_uuid(), conserved_val), level=2) node_weight = self.node_weight(magnitude, scenario) term = self.termination(scenario) # print('%6f %6f %s' % (magnitude, node_weight, self)) ff = [FragmentFlow(self, magnitude, node_weight, term, conserved)] if term.is_null or self.is_background or magnitude == 0: return ff, conserved_val ''' now looking forward: is our child node conserving? ''' if frags_seen is None: frags_seen = set() if self.reference_entity is None: if self.get_uuid() in frags_seen: raise InvalidParentChild('Frag %s seeing self\n %s' % (self.get_uuid(), '; '.join(frags_seen))) frags_seen.add(self.get_uuid()) # print('Traversing %s\nfrags seen: %s\n' % (self, '; '.join(frags_seen))) if term.is_fg or term.term_node.entity_type == 'process': ''' Handle foreground nodes and processes--> these can be quantity-conserving, but except for balancing flows the flow magnitudes are determined at the time of construction ''' stock = None bal_f = None if self._conserved_quantity is not None: stock = self.flow.cf(self._conserved_quantity) if self.reference_entity is None: # for reference nodes only, e.v. is inbound exchange-> scale balance back to real units stock *= self.exchange_value(scenario, observed=observed) # use repeat call to avoid double division if self.direction == 'Input': # convention: inputs to self are positive stock *= -1 _print('%.3s %g inbound-balance' % (self.get_uuid(), stock), level=2) for f in childflows(self): try: child_ff, cons = f.traverse(childflows, node_weight, scenario, observed=observed, frags_seen=set(frags_seen), conserved_qty=self._conserved_quantity) if cons is not None: stock += cons except BalanceFlowError: bal_f = f child_ff = [] ff.extend(child_ff) if bal_f is not None: # balance reports net inflows; positive value is more coming in than out # if balance flow is an input, its exchange must be the negative of the balance # if it is an output, its exchange must equal the balance if bal_f.direction == 'Input': stock *= -1 bal_ff, cons = bal_f.traverse(childflows, node_weight, scenario, observed=observed, frags_seen=set(frags_seen), conserved_qty=None, _balance=stock) ff.extend(bal_ff) else: ''' handle sub-fragments, including background flows-- for sub-fragments, the flow magnitudes are determined at the time of traversal and must be pushed out to child flows for background flows, the background ff should replace the current ff, except maintaining self as fragment ''' if term.term_node.is_background: bg_ff, cons = term.term_node.traverse(childflows, node_weight, scenario, observed=observed) bg_ff[0].fragment = self return bg_ff, conserved_val """ if target fragment is not reference flow, fragment needs to be rebased from traversal result. to do this: * follow reference entities to find the fragment's reference * when it comes back: - remove current flow from ios - create a new ios fragmentflow corresponding to the reference flow reversed * if we're aggregating, set this node's node weight to the downstream nw """ if term.term_node.reference_entity is not None: _print('inverse traversal---') the_ref = _recursive_ref(term.term_node) correct_reference = True in_ex = the_ref.exchange_value(scenario, observed=observed) # If flow directions conflict, subfragment is being run in reverse if term.term_node.direction == self.direction: in_ex *= -1 _print('%s\nNegating subfragments-- caution ahead!' % self, level=0) else: the_ref = term.term_node correct_reference = False in_ex = the_ref.exchange_value(scenario, observed=observed) # If flow directions conflict, subfragment is being run in reverse if term.term_node.direction != self.direction: in_ex *= -1 _print('%s\nNegating subfragments-- caution ahead!' % self, level=0) # for proper subfragments, need to determine child flow magnitudes based on traversal record subfrag_ffs, cons = the_ref.traverse(childflows, in_ex, scenario, observed=observed, frags_seen=set(frags_seen)) ios = [f for f in subfrag_ffs if f.term.is_null] subfrags = [f for f in subfrag_ffs if not f.term.is_null] ref_io = None if correct_reference: for _zz in ios: if _zz.fragment is term.term_node: in_ex = _zz.magnitude _print('%s' % _zz) _print('setting in_ex = %g' % in_ex) ios = [_zz for _zz in ios if _zz.fragment is not term.term_node] ref_io = GhostFragmentFlow(the_ref.flow, comp_dir(the_ref.direction)) ios.append(ref_io) # first, we determine subfragment activity level by adjusting for any autoconsumption matches = [f for f in ios if f.fragment.flow == term.term_flow] for m in matches: _print('+-%s' % m) if m.fragment.direction == term.direction: in_ex -= m.magnitude _print(' -= %g' % m.magnitude) else: in_ex += m.magnitude _print(' -= %g' % m.magnitude) ios.remove(m) downstream_nw = node_weight / abs(in_ex) # then we add the results of the subfragment, either in aggregated or disaggregated form if term.descend: # if appending, we are traversing in situ, so do scale _print('descending', level=0) for i in subfrags: i.scale(downstream_nw) ff.extend(subfrags) else: # if aggregating, we are only setting unit scores- so don't scale _print('aggregating', level=0) ff[0].term.aggregate_subfragments(subfrags) ff[0].node_weight = downstream_nw # next we traverse our own child flows, determining the exchange values from the subfrag traversal for f in childflows(self): ev = 0.0 matches = [j for j in ios if j.fragment.flow == f.flow] # exchange values are per unit- so don't scale for m in matches: if m is ref_io: _print('found it! %s' % m) if m.fragment.direction == f.direction: ev += m.magnitude else: ev -= m.magnitude ios.remove(m) child_ff, cons = f.traverse(childflows, downstream_nw, scenario, observed=observed, frags_seen=frags_seen, _balance=ev) ff.extend(child_ff) # remaining un-accounted io flows are getting appended, so do scale for x in ios: x.scale(downstream_nw) ff.extend(ios) # if descend is true- we give back everything- otherwise we aggregate return ff, conserved_val
def build_child_flows(self, fragment, scenario=None, background_children=False): """ Given a terminated fragment, construct child flows corresponding to the termination's complementary exchanges. :param fragment: the parent fragment :param scenario: :param background_children: if true, automatically terminate child flows to background. :return: """ if fragment.is_background: return None # no child flows for background nodes term = fragment.termination(scenario=scenario) if term.is_null or term.is_fg: return None if term.term_node.entity_type == 'process': if scenario is not None: print('Automatic building of child flows is not supported for scenario terminations of process nodes.') return [] if len([c for c in self.child_flows(fragment)]) != 0: print('Warning: fragment already has child flows. Continuing will result in (possibly many) ') if ifinput('duplicate child flows. Continue? y/n', 'n') != 'y': return [] int_exch = self.gen_exchanges(term.term_node, term.term_flow, term.direction) # in process case- possible to specify multiple identical children from the same node """ this means that build_child_flows will have undesirable results if called for a process- terminated node with similar exchanges for multiple scenarios. maybe changing node terminations should only be permitted for subfragments. something to think about. """ elif term.term_node.entity_type == 'fragment': if term.term_node.reference_entity is not None: the_ref = _recursive_ref(term.term_node) correct_reference = True else: the_ref = term.term_node correct_reference = False int_exch = self.get_fragment_inventory(the_ref, scenario=scenario) if correct_reference: surrogate_in = next(x for x in int_exch if x.flow is term.term_flow and x.direction == term.direction) # or StopIter int_exch.remove(surrogate_in) int_exch.append(ExchangeValue(the_ref, the_ref.flow, comp_dir(the_ref.direction), value=the_ref.cached_ev)) for x in int_exch: x.value *= 1.0 / surrogate_in.value # in subfragment case- child flows aggregate so we don't want to create duplicate children for x in int_exch: match = [c for c in self.child_flows(fragment) if c.flow == x.flow and c.direction == x.direction] if len(match) > 1: raise AmbiguousReference('Multiple child flows matching %s' % x) elif len(match) == 1: int_exch.remove(x) # don't make a new child if one is already found else: raise AmbiguousTermination('Cannot figure out entity type for %s' % term) children = [] for exch in int_exch: child = self[0].add_child_ff_from_exchange(fragment, exch) if background_children: self.terminate_to_background(child) children.append(child) return children
def terminate_fragment(self, index, frag, show=False): return self._check_exchanges(index, frag.flow, comp_dir(frag.direction), show=show)
def comp_dir(self): return comp_dir(self.exchange.direction)