def merge_equivalent_compounds(model):
def dicts_are_compatible(d1, d2):<EOL><INDENT>return all(key not in d1 or key not in d2 or d1[key] == d2[key]<EOL>for key in set(d1) | set(d2))<EOL><DEDENT>compound_compartment = {}<EOL>inelegible = set()<EOL>for reaction in model.reactions:<EOL><INDENT>equation = reaction.equation<EOL>if equation is None:<EOL><INDENT>continue<EOL><DEDENT>for compound, _ in equation.compounds:<EOL><INDENT>compartment = compound.compartment<EOL>if compartment is not None:<EOL><INDENT>compound_compartment[compound.name] = compartment<EOL>if not compound.name.endswith('<STR_LIT>'.format(compartment)):<EOL><INDENT>inelegible.add(compound.name)<EOL><DEDENT><DEDENT><DEDENT><DEDENT>compound_groups = {}<EOL>for compound_id, compartment in iteritems(compound_compartment):<EOL><INDENT>if compound_id in inelegible:<EOL><INDENT>continue<EOL><DEDENT>suffix = '<STR_LIT>'.format(compound_compartment[compound_id])<EOL>if compound_id.endswith(suffix):<EOL><INDENT>group_name = compound_id[:-len(suffix)]<EOL>compound_groups.setdefault(group_name, set()).add(compound_id)<EOL><DEDENT><DEDENT>compound_mapping = {}<EOL>merged_compounds = {}<EOL>for group, compound_set in iteritems(compound_groups):<EOL><INDENT>merged = []<EOL>for compound_id in compound_set:<EOL><INDENT>props = dict(model.compounds[compound_id].properties)<EOL>props.pop('<STR_LIT:id>', None)<EOL>props.pop('<STR_LIT>', None)<EOL>for merged_props, merged_set in merged:<EOL><INDENT>if dicts_are_compatible(props, merged_props):<EOL><INDENT>merged_set.add(compound_id)<EOL>merged_props.update(props)<EOL>break<EOL><DEDENT>else:<EOL><INDENT>keys = set(key for key in set(props) | set(merged_props)<EOL>if key not in props or<EOL>key not in merged_props or<EOL>props[key] != merged_props[key])<EOL>logger.info(<EOL>'<STR_LIT>'<EOL>'<STR_LIT>'.format(<EOL>compound_id, '<STR_LIT:U+002CU+0020>'.join(merged_set),<EOL>'<STR_LIT:U+002CU+0020>'.join(keys)))<EOL><DEDENT><DEDENT>else:<EOL><INDENT>merged.append((props, {compound_id}))<EOL><DEDENT><DEDENT>if len(merged) == <NUM_LIT:1>:<EOL><INDENT>merged_props, merged_set = merged[<NUM_LIT:0>]<EOL>for compound_id in merged_set:<EOL><INDENT>compound_mapping[compound_id] = group<EOL><DEDENT>merged_compounds[group] = merged_props<EOL><DEDENT>else:<EOL><INDENT>for merged_props, merged_set in merged:<EOL><INDENT>compartments = set(compound_compartment[c] for c in merged_set)<EOL>merged_name = '<STR_LIT>'.format(<EOL>group, '<STR_LIT:_>'.join(sorted(compartments)))<EOL>for compound_id in merged_set:<EOL><INDENT>compound_mapping[compound_id] = merged_name<EOL><DEDENT>merged_compounds[merged_name] = merged_props<EOL><DEDENT><DEDENT><DEDENT>for reaction in model.reactions:<EOL><INDENT>equation = reaction.equation<EOL>if equation is None:<EOL><INDENT>continue<EOL><DEDENT>reaction.equation = equation.translated_compounds(<EOL>lambda c: compound_mapping.get(c, c))<EOL><DEDENT>new_compounds = []<EOL>for compound in model.compounds:<EOL><INDENT>if compound.id not in compound_mapping:<EOL><INDENT>new_compounds.append(compound)<EOL><DEDENT>else:<EOL><INDENT>group = compound_mapping[compound.id]<EOL>if group not in merged_compounds:<EOL><INDENT>continue<EOL><DEDENT>props = merged_compounds.pop(group)<EOL>props['<STR_LIT:id>'] = group<EOL>new_compounds.append(DictCompoundEntry(<EOL>props, filemark=compound.filemark))<EOL><DEDENT><DEDENT>model.compounds.clear()<EOL>model.compounds.update(new_compounds)<EOL>new_exchange = OrderedDict()<EOL>for compound, reaction_id, lower, upper in itervalues(model.exchange):<EOL><INDENT>new_compound = compound.translate(<EOL>lambda name: compound_mapping.get(name, name))<EOL>new_exchange[new_compound] = new_compound, reaction_id, lower, upper<EOL><DEDENT>model.exchange.clear()<EOL>model.exchange.update(new_exchange)<EOL>
Merge equivalent compounds in various compartments.

    Tries to detect and merge compound entries that represent the same
    compound in different compartments. The entries are only merged if all
    properties are equivalent. Compound entries must have an ID with a suffix
    of an underscore followed by the compartment ID. This suffix will be
    stripped and compounds with identical IDs are merged if the properties
    are identical.

    Args:
        model: :class:`NativeModel`.