Exemple #1
0
    def __init__(self, fg_dir, cat_file=None, cfs=None, force_create_new=False):

        import time
        t0 = time.time()
        if fg_dir is None:
            self._catalog = CatalogInterface.new()
            self.db = FlowDB()
            print('Setup Catalog and FlowDB... (%.2f s)' % (time.time() - t0))

        else:
            self._ensure_foreground(fg_dir, force_create_new=force_create_new)
            if cat_file is None:
                cat_file = os.path.join(fg_dir, 'catalog.json')
            if os.path.exists(cat_file):
                self._catalog = CatalogInterface.new(fg_dir=fg_dir, catalog_file=cat_file)
            else:
                self._catalog = CatalogInterface.new(fg_dir=fg_dir)
            self._catalog.load(0)
            if os.path.exists(self[0].compartment_file):
                self.db = FlowDB(compartments=self[0].compartment_file)
            else:
                self.db = FlowDB()
            print('Setup Catalog and FlowDB... (%.2f s)' % (time.time() - t0))

            self[0].load_fragments(self._catalog)
            print('Fragments loaded... (%.2f s)' % (time.time() - t0))

        self._cfs = []
        self.unmatched_flows = defaultdict(set)
        if cfs is not None:
            print('Loading LCIA data... (%.2f s)' % (time.time() - t0))
            for c in cfs:
                self.load_lcia_cfs(c)
                print('finished %s... (%.2f s)' % (c, time.time() - t0))

        self.compute_unit_scores()
        print('finished... (%.2f s)' % (time.time() - t0))
Exemple #2
0
class ForegroundManager(object):
    """
    This class is used for building LCA models based on catalog refs.

    It consists of:

     * a catalog containing inventory and LCIA data
     * a Flow-Quantity database

    It manages:
     - adding and loading archives to the catalog
     - searching the catalog

    It maintains:
     - a result set generated from search
     - a select set for comparisons

    Fragments also get tacked on here for now

    The interface subclass provides UI for these activities
    """
    def __init__(self, fg_dir, cat_file=None, cfs=None, force_create_new=False):

        import time
        t0 = time.time()
        if fg_dir is None:
            self._catalog = CatalogInterface.new()
            self.db = FlowDB()
            print('Setup Catalog and FlowDB... (%.2f s)' % (time.time() - t0))

        else:
            self._ensure_foreground(fg_dir, force_create_new=force_create_new)
            if cat_file is None:
                cat_file = os.path.join(fg_dir, 'catalog.json')
            if os.path.exists(cat_file):
                self._catalog = CatalogInterface.new(fg_dir=fg_dir, catalog_file=cat_file)
            else:
                self._catalog = CatalogInterface.new(fg_dir=fg_dir)
            self._catalog.load(0)
            if os.path.exists(self[0].compartment_file):
                self.db = FlowDB(compartments=self[0].compartment_file)
            else:
                self.db = FlowDB()
            print('Setup Catalog and FlowDB... (%.2f s)' % (time.time() - t0))

            self[0].load_fragments(self._catalog)
            print('Fragments loaded... (%.2f s)' % (time.time() - t0))

        self._cfs = []
        self.unmatched_flows = defaultdict(set)
        if cfs is not None:
            print('Loading LCIA data... (%.2f s)' % (time.time() - t0))
            for c in cfs:
                self.load_lcia_cfs(c)
                print('finished %s... (%.2f s)' % (c, time.time() - t0))

        self.compute_unit_scores()
        print('finished... (%.2f s)' % (time.time() - t0))

    def load_lcia_cfs(self, nick, quantity=None):
        if self._catalog[nick] is None:
            self._catalog.load(nick)
        self.merge_compartments(nick)
        if quantity is None:
            self.unmatched_flows[nick] |= self.db.import_archive_cfs(self._catalog[nick])
        else:
            if not isinstance(quantity, LcQuantity):
                quantity = quantity.entity()  # assume catalog ref
            self.unmatched_flows[nick] |= self.db.import_quantity(self._catalog[nick], quantity)
        print('%d unmatched flows found from source %s... \n' %
              (len(self.unmatched_flows[nick]), self._catalog.name(nick)))
        self._cfs.append(nick)

    def show(self, loaded=True):
        if loaded:
            n = self._catalog.show_loaded()
            if n == 0:
                raise NoLoadedArchives('No archives loaded!')
        else:
            self._catalog.show()

    def show_all(self):
        self.show(loaded=False)

    def load(self, item):
        self._catalog.load(item)

    def add_archive(self, *args, **kwargs):
        self._catalog.add_archive(*args, **kwargs)

    def save(self):
        self._catalog.save_foreground()

    def flows(self, *args, elementary=False):
        for f in sorted(self[0].flows(), key=lambda x: x['Name']):
            if isinstance(elementary, bool):
                if not self.db.is_elementary(f) is elementary:
                    continue
            if len(args) != 0:
                if not bool(re.search(args[0], str(f), flags=re.IGNORECASE)):
                    continue
            print('%s' % f)

    def __getitem__(self, item):
        return self._catalog.__getitem__(item)

    def search(self, *args, show=True, **kwargs):
        return self._catalog.search(*args, show=show, **kwargs)

    def find_termination(self, ref, *args, index=None, **kwargs):
        if isinstance(ref, CatalogRef):
            if ref.entity_type == 'flow':
                return self._catalog.terminate_flow(ref, *args, **kwargs)
            else:
                print("don't know how to terminate %s" % ref)
        elif isinstance(ref, ExchangeRef):
            return self._catalog.terminate(ref, **kwargs)
        else:
            if index is None:
                print('Please specify an index.')
                return
            index = self._catalog.get_index(index)
            if isinstance(ref, Exchange):
                return self._catalog.terminate(ExchangeRef(self._catalog, index, ref), **kwargs)
            elif isinstance(ref, LcFlow):
                return self._catalog.terminate_flow(self.ref(index, ref), *args, **kwargs)
            elif ref.entity_type == 'fragment':
                return self._catalog.terminate_fragment(index, ref, **kwargs)

    def ref(self, *args):
        return self._catalog.ref(*args)

    @staticmethod
    def _ensure_foreground(folder, force_create_new=False):
        if not os.path.exists(folder):
            os.makedirs(folder)
        if force_create_new or not os.path.exists(os.path.join(folder, 'entities.json')):
            ForegroundArchive.new(folder)

    def _add_local_synonym(self, existing, new):
        syns = self._load_local_synonyms()
        syns[existing].append(new)
        self.db.flowables.add_synonym(existing, new)

    def _install_local_synonyms(self, syns):
        for k, v in syns:
            for val in v:
                self.db.flowables.add_synonym(k, val)

    def _load_local_synonyms(self):
        if os.path.exists(self[0].synonyms_file):
            with open(self[0].synonyms_file, 'r') as fp:
                syns = json.load(fp)
            return syns
        return defaultdict(list)

    def _save_local_synonyms(self, syns):
        with open(self[0].synonyms_file, 'w') as fp:
            json.dump(syns, fp)

    def workon(self, folder, force_create_new=False):
        """
        Select the current foreground.  Create folder if needed.
        If folder/entities.json does not exist, creates and saves a new foreground in folder.
        loads and installs archive from folder/entities.json
        :param folder:
        :param force_create_new: [False] if True, overwrite existing entities and fragments with a new
         foreground from the template.
        :return:
        """
        self._ensure_foreground(folder, force_create_new=force_create_new)
        self._catalog.reset_foreground(folder)
        self._catalog.load(0)
        if os.path.exists(self[0].catalog_file):
            self._catalog.open(self[0].catalog_file)
        if os.path.exists(self[0].compartment_file):
            self.db.load_compartments(self[0].compartment_file)

        self[0].load_fragments(self._catalog)
        self._load_local_synonyms()

        self.clear_unit_scores()
        self.compute_unit_scores()

    def add_to_foreground(self, ref):
        print('Add to foreground: %s' % ref)
        if isinstance(ref, CatalogRef):
            ref = ref.entity()
        if isinstance(ref, list):
            for k in ref:
                self.add_to_foreground(k)
        else:
            self._catalog[0].add_entity_and_children(ref)

    def merge_compartments(self, item):
        if self._catalog.is_loaded(0):
            self.db.save_compartments(self[0].compartment_file)
        index = self._catalog.get_index(item)
        for f in self[index].flows():
            if self._catalog.is_loaded(0):
                self.db.find_matching_compartment(f['Compartment'], interact=True)

    def find_flowable(self, string):
        return self.db.flowables.search(string)

    def add_synonym(self, name, flow):
        """
        Set a flow UUID to be a synonym with an entry in the flow db.
        :param name:
        :param flow:
        :return:
        """
        self._add_local_synonym(name, flow.get_uuid())

    def parse_flow(self, flow):
        return self.db.parse_flow(flow)

    def cfs_for_flowable(self, string, **kwargs):
        flowables = self.find_flowable(string)
        if len(flowables) > 1:
            for f in flowables:
                print('%6d %s [%s]' % (len(self.db.all_cfs(f, **kwargs)), self.db.flowables.name(f),
                                       self.db.flowables.cas(f)))
        elif len(flowables) == 1:
            f = flowables.pop()
            print('%s [%s]' % (self.db.flowables.name(f), self.db.flowables.cas(f)))
            for cf in self.db.all_cfs(f, **kwargs):
                print('%s' % cf)

    def cfs_for_flowable_grid(self, string, **kwargs):
        flowables = self.find_flowable(string)
        if len(flowables) > 1:
            for f in flowables:
                print('%6d %s [%s]' % (len([cf for cf in self.db.all_cfs(f, **kwargs)]),
                                       self.db.flowables.name(f),
                                       self.db.flowables.cas(f)))
        elif len(flowables) == 1:
            f = flowables.pop()
            self.db.cfs_for_flowable(f, **kwargs)

    # inspection methods
    def gen_exchanges(self, process_ref, ref_flow, direction):
        """
        This method takes in an exchange definition and gets all the complementary exchanges (i.e. the
        process_ref's intermediate exchanges, excluding the reference flow

        :param process_ref:
        :param ref_flow:
        :param direction:
        :return: an exchange generator (NOT ExchangeRefs because I haven't figured out how to be consistent on that yet)
        """
        for x in self.db.filter_exch(process_ref, elem=False, ref_flow=ref_flow):
            if not (x.flow == ref_flow and x.direction == direction):
                yield x

    def gen_elem(self, process_ref, ref_flow):
        for x in self.db.filter_exch(process_ref, elem=True, ref_flow=ref_flow):
            yield x

    def inventory(self, node, **kwargs):
        if node.entity_type == 'fragment':
            print('%s' % node)
            for x in sorted(self.get_fragment_inventory(node, **kwargs), key=lambda z: (z.direction, z.value)):
                print(x)
        else:
            print('%s' % node.fg())
            node.fg().inventory()

    def intermediate(self, process_ref, **kwargs):
        exch = self.db.filter_exch(process_ref, elem=False, **kwargs)
        if len(exch) == 0:
            print('No intermediate exchanges')
            return
        print('Intermediate exchanges:')
        for i in exch:
            print('%s' % i)

    def elementary(self, process_ref, **kwargs):
        exch = self.db.filter_exch(process_ref, elem=True, **kwargs)
        if len(exch) == 0:
            print('No elementary exchanges')
            return
        print('Elementary exchanges:')
        for i in exch:
            print('%s' % i)

    def compare_inventory(self, p_refs, elementary=False, **kwargs):
        def _key(exc):
            return ('%s [%s]' % (exc.flow['Name'], exc.flow.reference_entity.reference_entity.unitstring()),
                    exc.direction, exc.flow)
        ints = dict()
        int_set = set()
        elems = dict()
        elem_set = set()

        for p in p_refs:
            ints[p] = self.db.filter_exch(p, elem=False, **kwargs)
            int_set = int_set.union(_key(x) for x in ints[p])
            if elementary:
                elems[p] = self.db.filter_exch(p, elem=True, **kwargs)
                elem_set = elem_set.union(_key(x) for x in elems[p])

        int_rows = sorted(int_set, key=lambda x: x[1])
        int_flows = [x[2] for x in int_rows]

        dynamic_grid(p_refs, int_rows,
                     lambda x, y: {t for t in ints[y] if _key(t) == x},
                     ('Direction', lambda x: x[1]),
                     ('Flow', lambda x: x[0]),
                     returns_sets=True, suppress_col_list=elementary)

        if elementary:
            ele_rows = sorted(elem_set, key=lambda x: x[1])
            ele_flows = [x[2] for x in ele_rows]

            dynamic_grid(p_refs, ele_rows, lambda x, y: {t for t in elems[y] if _key(t) == x},
                         ('Direction', lambda x: x[1]),
                         ('Flow', lambda x: x[0]), returns_sets=True)
            return int_flows, ele_flows
        return int_flows

    def compare_allocation(self, p_ref):
        def _key(exc):
            return '%s' % exc.flow, exc.direction

        ints = dict()
        elems = dict()
        int_set = set()
        elem_set = set()
        cols = []
        for p in p_ref.entity().reference_entity:
            cols.append(p)
            ints[p] = self.db.filter_exch(p_ref, elem=False, ref_flow=p.flow)
            int_set = int_set.union(_key(x) for x in ints[p])
            elems[p] = self.db.filter_exch(p_ref, elem=True, ref_flow=p.flow)
            elem_set = elem_set.union(_key(x) for x in elems[p])

        int_rows = sorted(int_set, key=lambda x: x[1])

        dynamic_grid(cols, int_rows,
                     lambda x, y: {t for t in ints[y] if _key(t) == x},
                     ('Direction', lambda x: x[1]),
                     ('Flow', lambda x: x[0]), returns_sets=True, suppress_col_list=True)
        ele_rows = sorted(elem_set, key=lambda x: x[1])

        dynamic_grid(cols, ele_rows, lambda x, y: {t for t in elems[y] if _key(t) == x},
                     ('Direction', lambda x: x[1]),
                     ('Flow', lambda x: x[0]), returns_sets=True)

    @property
    def lcia_methods(self):
        return [q for q in self[0].lcia_methods()]

    @property
    def flow_properties(self):
        ls = set([q.get_uuid() for q in self.lcia_methods])
        return [q for q in self[0].quantities() if q.get_uuid() not in ls]

    def _rename_stage(self, frag, old, new):
        if frag['StageName'] == old:
            frag['StageName'] = new
        for x in self.child_flows(frag):
            self._rename_stage(x, old, new)

    def rename_stage(self, old_name, new_name, fragment=None):
        """
        Change the name of a stage everywhere it appears. By default, searches through all foreground fragments.
        you may also specify a single fragment, in which case it will apply only to that fragment and its descendents.
        :param old_name:
        :param new_name:
        :param fragment:
        :return:
        """
        if fragment is not None:
            fragments = [fragment]
        else:
            fragments = self[0].fragments(background=False)
        for f in fragments:
            self._rename_stage(f, old_name, new_name)

    def _prep_quantities(self, quantities):
        if quantities is None:
            qs = self._catalog[0].lcia_methods()
            if len(qs) == 0:
                print('No foreground LCIA methods')
                return None
            return qs
        if not (isinstance(quantities, list) or isinstance(quantities, tuple)):
            quantities = [quantities]
        out = []
        for k in quantities:
            if isinstance(k, CatalogRef):
                out.append(k.entity())
            else:
                out.append(k)
        return out

    def fg_lcia(self, process_ref, quantities=None, dist=3, scenario=None, **kwargs):
        """
        :param process_ref:
        :param quantities: defaults to foreground lcia quantities
        :param dist: [1] how far afield to search for cfs (see CLookup.find() from flowdb)
        :param scenario: (not presently used) - some day the flow-quantity database will be scenario-sensitive
        :return:
        """
        if self._catalog.fg is None:
            print('Missing a foreground!')
            return None
        if not self._catalog.is_loaded(0):
            self._catalog.load(0)
        if not self._catalog.is_loaded(process_ref.index):
            self._catalog.load(process_ref.index)
        exch = self.db.filter_exch(process_ref, elem=True, **kwargs)
        qs = self._prep_quantities(quantities)
        results = LciaResults(process_ref.entity())
        for q in qs:
            q_result = LciaResult(q)
            if q in self.db.known_quantities():
                for x in exch:
                    if not x.flow.has_characterization(q):
                        # look locally
                        local = self[0][x.flow.get_uuid()]
                        if local is not None and local.has_characterization(q):
                            cf = local.factor(q)
                            x.flow.add_characterization(cf)
                        else:
                            cf = self.db.lookup_single_cf(x.flow, q, dist=dist, location=process_ref['SpatialScope'])
                            if cf is None:
                                x.flow.add_characterization(q)
                            else:
                                x.flow.add_characterization(cf)
                                self.add_to_foreground(x.flow)
                    fac = x.flow.factor(q)
                    q_result.add_score(process_ref.id, x, fac, process_ref['SpatialScope'])
            results[q.get_uuid()] = q_result
        return results

    def bg_lcia(self, p_ref, quantities=None, **kwargs):
        qs = self._prep_quantities(quantities)
        if qs is None:
            return dict()

        if p_ref is None:
            # cutoff
            result = LciaResults(p_ref.entity())
            for q in quantities:
                result[q.get_uuid()] = LciaResult(q)
            return result
        return p_ref.archive.bg_lookup(p_ref.id, quantities=qs, flowdb=self.db, **kwargs)

    def compare_lcia_results(self, p_refs, quantities=None, background=False, **kwargs):
        """
        p_refs should be an array of catalog_refs
        :param p_refs:
        :param quantities: list of qs
        :param background: whether to use bg_lcia instead of fg_lcia
        :return:
        """
        results = dict()
        qs = self._prep_quantities(quantities)
        for p in p_refs:
            if background:
                results[p] = self.bg_lcia(p, quantities=quantities, **kwargs)
            else:
                results[p] = self.fg_lcia(p, quantities=quantities, **kwargs)

        dynamic_grid(p_refs, qs, lambda x, y: results[y][x.get_uuid()],
                     ('Ref Units', lambda x: x.reference_entity),
                     ('LCIA Method', lambda x: x['Name']))

    def lcia(self, p_ref):
        self.compare_lcia_results([p_ref])

    def show_detailed_lcia(self, p_ref, quantity, show_all=False):
        """

        :param p_ref:
        :param quantity:
        :param show_all: [False] show all exchanges, or only characterized exchanges
        :return:
        """
        result = self.fg_lcia(p_ref, quantity)[quantity.get_uuid()]
        result.show_details(p_ref, show_all=show_all)
        '''
        print('%s' % quantity)
        print('-' * 60)
        agg_lcia = result.LciaScores[p_ref.get_uuid()]
        for x in sorted(agg_lcia.LciaDetails, key=lambda t: t.result):
            if x.result != 0 or show_all:
                print('%10.3g x %-10.3g = %-10.3g %s' % (x.exchange.value, x.factor.value, x.result, x.factor.flow))
        print('=' * 60)
        print('             Total score: %g [%s]' % (agg_lcia.cumulative_result,
                                                     quantity.reference_entity.unitstring()))
        '''

    # fragment methods
    def _show_frag_children(self, frag, level=0):
        level += 1
        for k in self.child_flows(frag):
            print('%s%s' % ('  ' * level, k))
            self._show_frag_children(k, level)

    def fragments(self, *args, show_all=False, background=False, **kwargs):
        """

        :param background:
        :param show_all:
        :return:
        """
        for f in self[0].fragments(show_all=False, background=background, **kwargs):
            if len(args) != 0:
                if not bool(re.search(args[0], str(f), flags=re.IGNORECASE)):
                    continue
            print('%s' % f)
            if show_all:
                self._show_frag_children(f)

    def background(self, *args):
        self.fragments(*args, background=True)

    def frag(self, string, strict=True):
        """
        strict=True is slow
        :param string:
        :param strict:
        :return:
        """
        if strict:
            k = [f for f in self[0].fragments(show_all=True) if f.get_uuid().startswith(string.lower())]
            if len(k) > 1:
                for i in k:
                    print('%s' % i)
                raise AmbiguousReference()
            return k[0]
        else:
            return next(f for f in self[0].fragments(show_all=True) if f.get_uuid().startswith(string.lower()))

    def traverse(self, fragment, scenario=None, observed=False):
        if isinstance(fragment, str):
            fragment = self.frag(fragment)
        ffs = fragment.traversal_entry(lambda x: self.child_flows(x), scenario, observed=observed)
        return ffs

    def fragment_lcia(self, fragment, scenario=None, observed=False, scale=None):
        if isinstance(fragment, str):
            fragment = self.frag(fragment)
        r = fragment.fragment_lcia(lambda x: self.child_flows(x), scenario=scenario, observed=observed)
        if scale is not None:
            r.scale(scale)
        return r

    def draw(self, fragment, scenario=None, observed=False):
        if isinstance(fragment, str):
            fragment = self.frag(fragment)
        fs = fragment.show_tree(lambda x: self.child_flows(x), scenario=scenario, observed=observed)
        self.balance(fragment, scenario=scenario, observed=observed)
        return fs

    @staticmethod
    def _observe(c, scenario):
        if scenario is None:
            prompt = 'Observed value'
        else:
            prompt = 'Scenario value'

        print('%s' % c)
        print(' Cached EV: %6.4g\n Observed EV: %6.4g [%s]' % (c.cached_ev, c.observed_ev, c.flow.unit()))
        if scenario is None:
            string_ev = '%10g' % c.observed_ev
        else:
            string_ev = '%10g' % c.exchange_value(scenario)
            print(' Scenario EV: %s [%s]' % (string_ev,
                                             c.flow.unit()))
        val = ifinput('%s ("=" to use cached): ' % prompt, string_ev)
        if val != string_ev:
            if val == '=':
                new_val = c.cached_ev
            else:
                new_val = parse_math(val)
            if scenario is None:
                c.observed_ev = new_val
            else:
                c.set_exchange_value(scenario, new_val)

    def observe(self, fragment, scenario=None):
        if isinstance(fragment, str):
            fragment = self.frag(fragment)

        fragment.show()
        if scenario is None:
            print('{Observing for model reference}\n')
        else:
            print('{Observing for scenario "%s"}' % scenario)
        self._observe(fragment, scenario)
        if fragment.term.is_null:
            return
        if fragment.term.term_node.entity_type == 'fragment':
            print('fragment children are set by traversal')
            return
        for c in self.child_flows(fragment):
            self._observe(c, scenario)

    def child_flows(self, fragment):
        """
        This is a lambda method used during traversal in order to generate the child fragment flows from
        a given fragment.
        :param fragment:
        :return: fragments listing fragment as parent
        """
        for x in self[0].fragments(show_all=True):
            if fragment is x.reference_entity:
                yield x

    def scenarios(self, fragment=None, _scens=None):
        if fragment is None:
            scens = set()
            for i in self[0].fragments(show_all=True):
                scens = scens.union(self.scenarios(i))
            return scens

        if _scens is None:
            _scens = set()
        _scens = _scens.union(fragment.scenarios)
        for c in self.child_flows(fragment):
            _scens = self.scenarios(c, _scens=_scens)
        return _scens

    def show_scenario(self, scenario):
        print('Parameters for scenario "%s"' % scenario)
        for f in self[0].fragments(show_all=True, background=None):
            printed = False
            if f.exchange_value(scenario) != f.cached_ev:
                if printed is False:
                    printed = True
                    print('%s' % f)
                print(' Exchange value: %10.4g (default %10.4g)' % (f.exchange_value(scenario), f.cached_ev))
            if f.termination(scenario) != f.term:
                if printed is False:
                    print('%s' %f)
                print('%15s: %s\n%15s: %s' % ('Termination', f.termination(scenario).term_node.entity()['Name'],
                                              'Default', f.term.term_node.entity()['Name']))

    '''
    def auto_terminate(self, index, fragment, scenario=None, use_first=False):
        """

        :param index:
        :param fragment:
        :param scenario:
        :param use_first: [False] if True, resolve AmbiguousTerminations by using the first result
        :return:
        """
        term_exch = self._catalog.terminate_fragment(index, fragment)
        if len(term_exch) > 1 and not use_first:
            raise AmbiguousTermination('%d found' % len(term_exch))
        elif len(term_exch) == 0:
            raise NoTermination
        term_exch = term_exch[0]
        try:
            bg = next(f for f in self[0].fragments(background=True) if f.term.matches(term_exch))
            fragment.terminate(bg, scenario=scenario)
        except StopIteration:
            fragment.term_from_exch(term_exch, scenario=scenario)
            self.build_child_flows(fragment, scenario=scenario)
    '''

    def import_fragment(self, filename):
        if os.path.isdir(filename):
            print('Loading fragment files in directory %s' % filename)
            self[0].import_fragments(self._catalog, filename)
        else:
            self[0].import_fragment(self._catalog, filename)

    def terminate_to_foreground(self, fragment, scenario=None):
        """
        Marks a fragment as its own termination. Note: this creates unresolved issues with flow matching across
        scenarios. but those have always been there and never been solved.
        If the termination already exists, then the *elementary* flows are made into child flows (assuming the
        intermediate flows were already done).
        If the fragment was terminated to a sub-fragment, nothing happens.. the subfragment link is just deleted.
        any i/o flows developed from the subfragment remain, but their exchange values will be used as-specified.
        :param fragment:
        :return:
        """
        if isinstance(fragment, str):
            fragment = self.frag(fragment)
        term = fragment.termination(scenario)
        children = []
        if term.is_fg:
            return children
        if (not term.is_null) and term.term_node.entity_type == 'process':
            fragment.scale_evs(1.0 / term.inbound_exchange_value)
            for elem in self.gen_elem(term.term_node, term.term_flow):
                child = self[0].add_child_ff_from_exchange(fragment, elem, Name=str(fragment.flow),
                                                           StageName='direct emission')
                child.term.self_terminate()
                children.append(child)
        term.self_terminate()
        return children

    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 get_fragment_inventory(self, fragment, scenario=None, scale=None, observed=False):
        """
        Aggregates inputs and outputs (un-terminated flows) from a fragment; returns a list of exchanges.
        :param fragment: ff
        :param scenario:
        :return:
        """
        io_ffs = fragment.io_flows(lambda x: self.child_flows(x), scenario, observed=observed)
        if scale is not None:
            for i in io_ffs:
                i.scale(scale)

        accum = defaultdict(float)
        ent = dict()
        ev = fragment.exchange_value(scenario)
        if fragment.direction == 'Input':  # this is input to parent flow, so output to us
            ev = -ev
        accum[fragment.flow.get_uuid()] = ev
        for i in io_ffs:
            ent[i.fragment.flow.get_uuid()] = i.fragment.flow
            if i.fragment.direction == 'Input':
                accum[i.fragment.flow.get_uuid()] += i.magnitude
            else:
                accum[i.fragment.flow.get_uuid()] -= i.magnitude

        in_ex = accum.pop(fragment.flow.get_uuid())
        if in_ex * ev < 0:  # i.e. if the signs are different
            raise ValueError('Fragment requires more reference flow than it generates')
        frag_exchs = []
        for k, v in accum.items():
            val = abs(v)
            if fragment.reference_entity is None:
                if ev != in_ex:
                    val *= (ev / in_ex)
            if v == 0:
                continue
            elif v < 0:
                dirn = 'Output'
            else:
                dirn = 'Input'
            frag_exchs.append(ExchangeValue(fragment, ent[k], dirn, value=val))
        return sorted(frag_exchs, key=lambda x: x.direction)

    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 clear_unit_scores(self, scenario=None):
        for f in self[0].fragments(show_all=True):
            if scenario is None:
                for term in f.terminations():
                    f.termination(term).clear_score_cache()
            else:
                if scenario in f.terminations():
                    f.termination(scenario).clear_score_cache()

    def compute_unit_scores(self, scenario=None):
        for f in self[0].fragments(show_all=True):
            self.compute_fragment_unit_scores(f, scenario=scenario)

    '''
    if self.db.is_elementary(fragment.flow):
        cfs = self.db.factors_for_flow(fragment.term.term_flow, [l for l in self[0].lcia_methods()])
        fragment.term.flowdb_results(LciaResult.from_cfs(fragment, cfs))
    '''

    def compute_fragment_unit_scores(self, fragment, scenario=None):
        """
        lcia gets run as: lcia(self.term_node, self.term_flow, q_run)
        for background or default: x = fragment
        :param fragment:
        :param scenario:
        :return:
        """
        term = fragment.termination(scenario)
        l_methods = [q for q in self[0].lcia_methods()]
        if fragment.is_background:
            def lcia(x, y, z):
                return self.bg_lcia(x, ref_flow=y, quantities=z)
        elif term.is_fg:
            def lcia(x, y, z):
                local = self[0][y.get_uuid()]
                if local is not y:
                    raise AmbiguousReference('!!! this should be in foreground')
                return LciaResult.from_cfs(x, self.db.factors_for_flow(local, z))
        else:
            def lcia(x, y, z):
                return self.fg_lcia(x, ref_flow=y, quantities=z, scenario=scenario)
        term.set_score_cache(lcia, l_methods)

    def terminate_to_background(self, fragment):
        """
        Given an existing fragment, create (or locate) a background reference that terminates it. If the fragment is
        terminated, transfer the termination to the background reference.
        :param fragment:
        :return:
        """
        if fragment.term.is_bg:
            return fragment  # nothing to do
        else:
            if fragment.term.is_null:
                frag_exch = Exchange(fragment, fragment.flow, fragment.direction)
                try:
                    bg = next(f for f in self[0].fragments(background=False) if f.term.terminates(frag_exch))
                    print('found termination in foreground')
                    fragment.terminate(bg)
                except StopIteration:
                    try:
                        bg = next(f for f in self[0].fragments(background=True) if f.term.terminates(frag_exch))
                        print('found cutoff flow')
                        fragment.terminate(bg)
                    except StopIteration:
                        bg = self[0].add_background_ff_from_fragment(fragment)
                        print('created null termination in background')
                        fragment.terminate(bg)
            else:
                term_exch = fragment.term.to_exchange()
                try:
                    bg = next(f for f in self[0].fragments(background=False) if f.term.matches(term_exch))
                    print('found termination match in foreground')
                    fragment.terminate(bg)
                except StopIteration:
                    try:
                        bg = next(f for f in self[0].fragments(background=True) if f.term.matches(term_exch))
                        print('found termination match in background')
                        fragment.terminate(bg)
                    except StopIteration:
                        bg = self[0].add_background_ff_from_fragment(fragment)
                        print('created termination in background')
                        fragment.terminate(bg)
        return bg

    def fragment_to_foreground(self, fragment, background_children=True):
        """
        Move a background fragment into the foreground. Add the node's child flows to the foreground.

        Given a fragment that is terminated to background, recall the background termination and make it into a
          foreground termination.  (the background reference will not be deleted).  Proceed to add the foreground
          node's child flows.
        :param fragment:
        :param background_children:
        :return:
        """
        if fragment.is_background:
            fragment.to_foreground()
            self.build_child_flows(fragment, background_children=background_children)
            return fragment
        elif fragment.term.is_bg:
            bg = fragment.term.term_node
            fragment.terminate(bg.term.term_node, flow=bg.term.term_flow, direction=bg.term.direction)
            self.build_child_flows(fragment, background_children=background_children)
            return fragment
        elif fragment.term.is_null:
            fragment.term.self_terminate()
        return fragment  # nothing to do

    def balance(self, frag, scenario=None, observed=False):
        qs = defaultdict(float)
        if frag.reference_entity is None:
            in_ex = frag.exchange_value(scenario, observed=observed)
        else:
            in_ex = 1.0
        for cf in frag.flow.characterizations():
            if cf.value is not None:
                if frag.direction == 'Input':  # output from term
                    qs[cf.quantity] -= cf.value * in_ex
                else:
                    qs[cf.quantity] += cf.value * in_ex
        for c in self.child_flows(frag):
            for cf in c.flow.characterizations():
                mag = c.exchange_value(scenario, observed=observed) * (cf.value or 0.0)
                if mag != 0:
                    if c.direction == 'Output':
                        qs[cf.quantity] -= mag
                    else:
                        qs[cf.quantity] += mag

        for k, v in qs.items():
            print('%10.4g %s' % (v, k))
        return qs

    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)