class Spec(ABC):
    """Spec is the abstract superclass for ProteinSpec, GeneSpec and MRNASpec."""

    def __init__(self, name: str, struct_index: Optional[int], locus: 'Locus') -> None:
        self.name, self.struct_index, self.locus = name, struct_index, locus
        self._validate()

    def __hash__(self) -> int:
        return hash(str(self))

    def __repr__(self) -> str:
        return str(self)

    def __str__(self) -> str:
        def struct_name(spec: Spec, spec_suffix: 'SpecSuffix') -> str:
            if spec.struct_index is not None:
                return "{0}{1}@{2}".format(spec.name, spec_suffix.value, spec.struct_index)
            else:
                return "{0}{1}".format(spec.name, spec_suffix.value)

        suffix = SPEC_TO_SUFFIX[type(self)]

        if str(self.locus):
            return '{0}_[{1}]'.format(struct_name(self, suffix), str(self.locus))
        else:
            return '{0}'.format(struct_name(self, suffix))

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Spec):
            return NotImplemented
        return isinstance(other, type(self)) and self.name == other.name and self.locus == other.locus and \
               self.struct_index == other.struct_index

    def __lt__(self, other: 'Spec') -> bool:
        return str(self) < str(other)

    def clone(self) -> 'Spec':
        return deepcopy(self)

    def is_subspec_of(self, other: 'Spec') -> bool:
        """Specs have a subset / superset relation, e.g. A_[(r)] is a subset of A."""
        if self == other:
            return True

        spec_pairs = zip([self.name, self.locus.domain, self.locus.subdomain, self.locus.residue],
                         [other.name, other.locus.domain, other.locus.subdomain, other.locus.residue])

        for my_property, other_property in spec_pairs:
            if my_property and other_property and my_property != other_property:
                return False
            elif not my_property and other_property:
                return False

        return True

    def is_superspec_of(self, other: 'Spec') -> bool:
        """Specs have a subset / superset relation, e.g. A is a superset of A_[(r)]."""
        if self == other:
            return True

        return other.is_subspec_of(self)

    @property
    def is_structured(self) -> bool:
        return self.struct_index is not None

    @property
    def is_component_spec(self) -> bool:
        """A ComponentSpec is a Spec without any Locus information."""
        return self.has_resolution(LocusResolution.component)

    def with_name_suffix(self, suffix: str) -> 'Spec':
        # Assure the suffix is not reserved, i.e. 'Gene', 'mRNA'
        try:
            SpecSuffix(suffix)
            raise SyntaxError('Cannot use reserved suffix {} in Spec.with_name_suffix'.format(suffix))
        except ValueError:
            return type(self)(self.name + suffix, self.struct_index, self.locus)

    def with_struct_index(self, index: int) -> 'Spec':
        """Returns a new Spec of the same type with a given struct index."""
        return type(self)(self.name, index, self.locus)

    def with_struct_from_spec(self, other: 'Spec') -> 'Spec':
        """Returns a new Spec of the same type with a struct index matching the one carried by the Spec given."""
        assert other.struct_index is not None
        assert self.name == other.name
        return type(self)(self.name, other.struct_index, self.locus)

    def with_locus(self, locus: 'Locus') -> 'Spec':
        """Returns a new Spec of the same type with a given Locus."""
        return type(self)(self.name, self.struct_index, locus.clone())

    def with_domain(self, domain: str) -> 'Spec':
        """Returns a new Spec of the same type with its domain changed into the one given."""
        return type(self)(self.name, self.struct_index, self.locus.with_domain(domain))

    def to_non_struct_spec(self) -> 'Spec':
        """Returns a new Spec of the same type without struct index."""
        return type(self)(self.name, None, self.locus)

    def to_component_spec(self) -> 'Spec':
        """Returns a new Spec of the same type without Locus."""
        return type(self)(self.name, self.struct_index, EmptyLocus())

    def to_protein_component_spec(self) -> 'ProteinSpec':
        """Returns a new ProteinSpec with the same name, without structure index or Locus."""
        return ProteinSpec(self.name, None, EmptyLocus())

    def to_gene_component_spec(self) -> 'GeneSpec':
        """Returns a new GeneSpec with the same name, without structure index or Locus."""
        return GeneSpec(self.name, None, EmptyLocus())

    def to_mrna_component_spec(self) -> 'MRNASpec':
        """Returns a new MRNASpec with the same name, without structure index or Locus."""
        return MRNASpec(self.name, None, EmptyLocus())

    @property
    def resolution(self) -> 'LocusResolution':
        return self.locus.resolution

    def has_resolution(self, resolution: 'LocusResolution') -> bool:
        return self.resolution == resolution

    def _validate(self) -> None:
        assert self.name is not None and re.match(r'\w+', self.name) class LocusResolution(Enum):
    component = 'component'
    domain = 'domain'
    subdomain = 'subdomain'
    residue = 'residue'

    def __lt__(self, other: object) -> bool:
        if not isinstance(other, LocusResolution):
            return NotImplemented

        if self == LocusResolution.component:
            return other != LocusResolution.component
        elif self == LocusResolution.domain:
            return other in [LocusResolution.subdomain, LocusResolution.residue]
        elif self == LocusResolution.subdomain:
            return other == LocusResolution.residue
        else:
            return False class MRNASpec(Spec):
    def __hash__(self) -> int:
        return hash(str(self)) def spec_from_str(spec_str: str) -> Spec:
    """Returns a Spec object parsed from the given string, raises SyntaxError if not parsable."""
    def spec_from_suffixed_name_and_locus(name: str, struct_index: Optional[int],
                                          locus: Locus) -> Spec:  # pylint: disable=invalid-name
        for suffix in SUFFIX_TO_SPEC:
            if name.endswith(suffix.value):
                name = name[:len(name) - len(suffix.value)]
                return SUFFIX_TO_SPEC[suffix](name, struct_index, locus)
            elif name.lower().endswith(suffix.value.lower()):
                raise SyntaxError(
                    'Please use correct capitalization \'{}\' in component \'{}\'.'.format(suffix.value, name))

        raise SyntaxError('Could not parse spec component_name {}'.format(name))

    if not re.match(r'[A-Za-z][A-Za-z0-9]*(@[\d]+)*(_\[[[A-Za-z0-9]/\(\)]+\])*', spec_str):
        raise SyntaxError('Spec str {} does not match validating regex.'.format(spec_str))

    struct_index = None
    items = spec_str.split('_', maxsplit=1)

    if not re.match(r'[a-zA-Z]', items[0]):
        raise SyntaxError('Spec has to start with letter character.')
    elif '@' in items[0]:
        name, struct_index_str = items[0].split('@')
        struct_index = int(struct_index_str)
    else:
        name = items[0]

    if len(items) == 1:
        return spec_from_suffixed_name_and_locus(name, struct_index, EmptyLocus())
    elif len(items) == 2:
        return spec_from_suffixed_name_and_locus(name, struct_index, locus_from_str(items[1]))
    else:
        raise SyntaxError('Could not parse spec string {}'.format(spec_str)) def state_from_str(state_str: str) -> State:  # pylint: disable=too-many-return-statements
    state_str = state_str.strip()

    if re.match(GLOBAL_STATE_REGEX, state_str):
        return GlobalState(state_str)
    elif re.match(INTERACTION_STATE_REGEX, state_str):
        first_str, second_str = re.findall(INTERACTION_STATE_REGEX, state_str)[0]
        return InteractionState(spec_from_str(first_str), spec_from_str(second_str))
    elif re.match(EMPTY_BINDING_STATE_REGEX, state_str):
        spec_str = re.findall(EMPTY_BINDING_STATE_REGEX, state_str)[0]
        return EmptyBindingState(spec_from_str(spec_str))
    elif re.match(SELF_INTERACTION_STATE_REGEX, state_str):
        first_str, second_str = re.findall(SELF_INTERACTION_STATE_REGEX, state_str)[0]
        first_spec = spec_from_str(first_str)
        second_spec = spec_from_str(first_str).with_locus(locus_from_str(second_str))
        return SelfInteractionState(first_spec, second_spec)
    elif re.match(GLOBAL_STATE_REGEX, state_str):
        return GlobalState(state_str.strip('[]'))
    elif re.match(MODIFICATION_STATE_REGEX, state_str):
        spec_str, mod_str = re.findall(MODIFICATION_STATE_REGEX, state_str)[0]
        return ModificationState(spec_from_str(spec_str), state_modifier_from_str(mod_str))
    elif re.match(FULLY_NEUTRAL_STATE_REGEX, state_str):
        return FullyNeutralState()
    else:
        raise SyntaxError('Unable to parse State string {}'.format(state_str)) class Quick:
    def __init__(self, rxncon_str: str) -> None:
        self.quick_input = rxncon_str.split('\n')
        self._rxncon_system = None              # type: Optional[RxnConSystem]
        self._reactions = []                    # type: List[Reaction]
        self._contingencies = []                # type: List[Contingency]
        self._contingency_list_entries = []     # type: List[ContingencyListEntry]
        self._parse_str()
        self._construct_contingencies()
        self._construct_rxncon_system()
        assert self._rxncon_system is not None

    @property
    def rxncon_system(self) -> RxnConSystem:
        assert self._rxncon_system is not None
        return self._rxncon_system

    def _parse_str(self) -> None:
        BOOL_REGEX = '^\<.+?\>$'

        for line in self.quick_input:
            reaction_string = line.split(';')[0].strip()
            contingency_strings = line.split(';')[1:]
            if reaction_string:
                if not re.match(BOOL_REGEX, reaction_string):
                    self._add_reaction_from_string(reaction_string)
                self._add_contingency_list_entries(contingency_strings, reaction_string)

    def _add_reaction_from_string(self, reaction_str: str) -> None:
        reaction_strs = split_bidirectional_reaction_str(reaction_str)
        for rxn in reaction_strs:
            reaction = reaction_from_str(rxn)
            self._reactions.append(reaction)

    def _add_contingency_list_entries(self, contingency_strs: List[str], reaction_str: str) -> None:
        for cont in contingency_strs:
            cont = cont.strip()
            cont_type = cont.split()[0]
            modifier = cont.split()[-1]

            #  If the verb is bidirectional, only apply the contingency to the forward direction.
            reaction_strs = split_bidirectional_reaction_str(reaction_str)
            entry = contingency_list_entry_from_strs(reaction_strs[0], cont_type, modifier)
            self._contingency_list_entries.append(entry)

    def _construct_contingencies(self) -> None:
        self._contingencies = contingencies_from_contingency_list_entries(self._contingency_list_entries)

    def _construct_rxncon_system(self) -> None:
        self._rxncon_system = RxnConSystem(self._reactions, self._contingencies) def complex_from_str(complex_str: str) -> Complex:
    """Constructs a Complex object from a BNGL complex string."""
    return Complex([mol_from_str(x) for x in complex_str.split('.')]) def rule_from_str(rule_str: str) -> Rule:
    """Constructs a Rule object from a BNGL rule string."""
    lhs_strs = []  # type: List[str]
    rhs_strs = []  # type: List[str]
    rate = None  # type: Optional[Parameter]

    current = lhs_strs
    done = False

    for part in rule_str.split():
        if part == '+':
            done = False
            continue
        elif part == '->':
            done = False
            current = rhs_strs
        elif not done:
            current.append(part)
            done = True
        elif done:
            rate = Parameter(part, None)

    assert rate is not None
    return Rule([complex_from_str(x) for x in lhs_strs], [complex_from_str(x) for x in rhs_strs], rate) def rule_based_model_from_rxncon(rxncon_sys: RxnConSystem) -> RuleBasedModel:  # pylint: disable=too-many-locals
    """Returns a RBM given a rxncon system."""

    def mol_defs_from_rxncon(rxncon_sys: RxnConSystem) -> List[MolDef]:
        mol_defs = {}
        for spec in rxncon_sys.components():
            LOGGER.debug('mol_defs_from_rxncon : Creating MolDefBuilder for {}'.format(str(spec)))
            builder = MolDefBuilder(spec)
            for state in rxncon_sys.states_for_component(spec):
                LOGGER.debug('mol_defs_from_rxncon : Applying State {} of type {}'
                             .format(str(state), type(state)))
                for func in STATE_TO_MOL_DEF_BUILDER_FN[type(state)]:  # type: ignore
                    func(state, builder)

            mol_defs[spec] = builder.build()

        return list(mol_defs.values())

    def remove_global_states(solutions: List[Dict[State, bool]]) -> List[Dict[State, bool]]:
        filtered_solutions = []  # type: List[Dict[State, bool]]
        for soln in solutions:
            cleaned_solution = {}
            for state, val in soln.items():
                if state.is_global:
                    LOGGER.warning('remove_global_states : REMOVING INPUT STATE {} from contingencies.'
                                   .format(state))
                else:
                    cleaned_solution[state] = val

            if cleaned_solution not in filtered_solutions:
                filtered_solutions.append(cleaned_solution)

        return filtered_solutions

    def is_satisfiable(states: Iterable[State]) -> bool:
        for pair in combinations(states, 2):
            if pair[0].is_mutually_exclusive_with(pair[1]):
                return False

        return True

    def calc_positive_solutions(rxncon_sys: RxnConSystem, solution: Dict[State, bool]) -> List[List[State]]:
        def complementary_state_combos(state: State) -> List[List[State]]:
            combos = product(
                *(rxncon_sys.complement_states_for_component(spec.to_component_spec(), state) for spec in
                  state.specs))
            return [list(combo) for combo in combos if is_satisfiable(combo)]

        def structure_states(states: List[State]) -> List[State]:
            cur_index = max(spec.struct_index for state in states for spec in state.specs if spec.is_structured)
            assert cur_index is not None

            spec_to_index = {}  # type: Dict[Spec, int]
            struct_states = []  # type: List[State]

            for state in states:
                if state.is_structured:
                    struct_states.append(state)
                    continue

                for spec in state.specs:
                    if spec.is_structured:
                        continue

                    try:
                        state = state.to_structured_from_spec(
                            spec.with_struct_index(spec_to_index[spec.to_component_spec()]))
                    except KeyError:
                        cur_index += 1
                        state = state.to_structured_from_spec(spec.with_struct_index(cur_index))
                        spec_to_index[spec.to_component_spec()] = cur_index

                struct_states.append(state)

            return struct_states

        ordered_solution = OrderedDict(sorted(solution.items(), key=lambda x: x[0]))

        trues = [state for state, val in ordered_solution.items() if val]
        falses = [state for state, val in ordered_solution.items() if not val
                  and not any(state.is_mutually_exclusive_with(x) for x in trues)]

        if not falses:
            return [trues] if is_satisfiable(trues) else []

        positivized_falses = [list(chain(*x)) for x in
                              product(*(complementary_state_combos(state) for state in falses))]

        solutions = []

        for positivized_false in positivized_falses:
            possible_solution = []  # type: List[State]
            for soln_state in structure_states(trues + positivized_false):
                if soln_state not in possible_solution:
                    possible_solution.append(soln_state)

            if is_satisfiable(possible_solution):
                solutions.append(possible_solution)

        return solutions

    def calc_rule(reaction: Reaction, cont_soln: List[State], quant_cont: VennSet[State]) -> Rule:
        def calc_complexes(terms: List[ReactionTerm], states: List[State]) -> List[Complex]:
            if not all(x.is_structured for x in states):
                unstructs = [x for x in states if not x.is_structured]
                raise AssertionError('Error in building rule for Reaction {}, States {} appear unstructured'
                                     .format(str(reaction), ', '.join(str(x) for x in unstructs)))

            if not is_satisfiable(cont_soln):
                raise AssertionError(
                    'Cannot satisfy contingencies {} simultaneously'.format(' & '.join(str(s) for s in cont_soln)))

            states = copy(states)
            builder = ComplexExprBuilder(reaction=reaction)
            struct_index = 0
            for term in terms:
                struct_states = deepcopy(term.states)
                for spec in term.specs:
                    struct_spec = copy(spec)
                    struct_spec.struct_index = struct_index
                    builder.add_mol(struct_spec, is_reactant=True)
                    struct_states = [state.to_structured_from_spec(struct_spec) for state in struct_states]
                    struct_index += 1

                states += struct_states

            assert all(x.is_structured for x in states)

            for state in states:
                for func in STATE_TO_COMPLEX_BUILDER_FN[type(state)]:  # type: ignore
                    func(state, builder)

            return builder.build()

        lhs = calc_complexes(reaction.terms_lhs, cont_soln)
        rhs = calc_complexes(reaction.terms_rhs, cont_soln)

        rate = Parameter('k_{}{}'.format(rxncon_sys.reaction_number(reaction) + 1, quant_cont.rate_constant_desc),
                         '1.0',
                         description=str(reaction))

        return Rule(lhs, rhs, rate, parent_reaction=reaction, quant_cont=quant_cont)

    def calc_initial_conditions(mol_defs: List[MolDef]) -> List[InitialCondition]:
        return \
            [InitialCondition(mol_def.create_neutral_complex(),
                              Parameter('Num{}'.format(mol_def.name), str(INITIAL_MOLECULE_COUNT))) for mol_def in
             mol_defs]

    def calc_observables(rxncon_sys: RxnConSystem) -> List[Observable]:
        def observable_complex(states: List[State]) -> Complex:
            builder = ComplexExprBuilder()

            assert all(x.is_structured for x in states), 'Unstructured states appearing in observable ' \
                                                         '{}'.format(states)

            for state in states:
                for func in STATE_TO_COMPLEX_BUILDER_FN[type(state)]:  # type: ignore
                    func(state, builder)

            complexes = builder.build(only_reactants=False)

            assert len(complexes) == 1, 'Multiple complexes appearing in observable {}'.format(states)
            return complexes[0]

        observables = []
        output_rxns = [rxn for rxn in rxncon_sys.reactions if isinstance(rxn, OutputReaction)]
        for rxn in output_rxns:
            LOGGER.debug('calc_observables : calculating observable {}'.format(str(rxn)))
            solns = Intersection(*(x.to_venn_set() for x
                                   in rxncon_sys.contingencies_for_reaction(rxn))).calc_solutions()
            positive_solns = []  # type: List[List[State]]
            for soln in solns:
                positive_solns += calc_positive_solutions(rxncon_sys, soln)

            for index, positive_soln in enumerate(positive_solns):
                LOGGER.debug('calc_observables : solution {} : {}'.format(index, positive_soln))
                observables.append(Observable('{}{}'.format(rxn.name, index), observable_complex(positive_soln)))

        return observables

    def add_structure_to_negative_interaction_states(states):
        """adding structure index to components connected through complements to empty domains"""
        m = 0
        unstructured = []
        for sol in positive_solutions:
            for s in sol:
                if isinstance(s, InteractionState):
                    if s.first.is_structured:
                        m = max(m, s.first.struct_index)
                    else:
                        if s.first not in unstructured:
                            unstructured.append(s.first)
                    if s.second.is_structured:
                        m = max(m, s.second.struct_index)
                    else:
                        if s.second not in unstructured:
                            unstructured.append(s.second)

        for comp in unstructured:
            m += 1
            comp.struct_index = m

        return (states)

    mol_defs = mol_defs_from_rxncon(rxncon_sys)
    LOGGER.debug(
        'rule_based_model_from_rxncon : Generated MolDefs: {}'.format(', '.join(str(mol_def) for mol_def in mol_defs)))

    rules = []  # type: List[Rule]

    for reaction in (x for x in rxncon_sys.reactions if not isinstance(x, OutputReaction)):
        LOGGER.debug('rule_based_model_from_rxncon : Generating rules for reaction {}'.format(str(reaction)))
        strict_cont_set = Intersection(*(x.to_venn_set() for x in
                                         rxncon_sys.s_contingencies_for_reaction(reaction)))  # type: VennSet[State]
        quant_contingencies = QuantContingencyConfigs(rxncon_sys.q_contingencies_for_reaction(reaction))
        LOGGER.debug('rule_based_model_from_rxncon : Strict contingencies {}'.format(str(strict_cont_set)))

        found_solution = False

        for quant_contingency_set in quant_contingencies:
            LOGGER.debug('rule_based_model_from_rxncon : quantitative contingency config: {}'
                         .format(str(quant_contingency_set)))

            cont_set = Intersection(strict_cont_set, quant_contingency_set)  # type: VennSet[State]
            LOGGER.debug('rule_based_model_from_rxncon : adding constraints...')
            cont_set = with_connectivity_constraints(cont_set, rxncon_sys)
            LOGGER.debug('rule_based_model_from_rxncon {} : calculating solutions...'.format(datetime.now()))
            solutions = cont_set.calc_solutions()
            LOGGER.debug('rule_based_model_from_rxncon {} : found {} non-positivized solutions...'
                         .format(datetime.now(), len(solutions)))
            solutions = remove_global_states(solutions)

            LOGGER.debug('rule_based_model_from_rxncon : contingency solutions {}'.format(str(solutions)))
            positive_solutions = []  # type: List[List[State]]
            for solution in solutions:
                positive_solutions += calc_positive_solutions(rxncon_sys, solution)

            positive_solutions = add_structure_to_negative_interaction_states(positive_solutions)
            """added to add structure to binding partners of empty domains"""

            for positive_solution in positive_solutions:
                found_solution = True
                LOGGER.debug('rule_based_model_from_rxncon : positivized contingency solution {}'
                             .format(' & '.join(str(x) for x in positive_solution)))
                rule = calc_rule(reaction, positive_solution, quant_contingency_set)
                if not any(rule.is_equivalent_to(existing) for existing in rules):
                    rules.append(rule)

        if not found_solution:
            LOGGER.error('rule_based_model_from_rxncon : could not find positive solutions for rxn {}'
                         .format(str(reaction)))

    return RuleBasedModel(mol_defs, calc_initial_conditions(mol_defs), [], calc_observables(rxncon_sys), rules) def initial_condition_from_str(ic_str: str) -> InitialCondition:
    """Constructs an InitialCondition object from a BNGL initial condition string."""
    complex_str, param = ic_str.split()
    return InitialCondition(complex_from_str(complex_str), Parameter(param, None)) def bond_complexes(cont_set: VennSet[State]) -> List[BondComplex]:
    """Returns a list of BondComplex objects, describing the valid complexes
    that could be built out of a contingency."""
    bond_filter = make_bond_filter(cont_set)
    comp_to_bonds = group_states(cont_set, lambda s: bond_filter(s))

    single_components = set()  # type: Set[BondComplex]

    LOGGER.debug('bond_complexes {} : building single-molecule bond configurations'.format(datetime.now()))
    for comp, bonds in comp_to_bonds.items():
        LOGGER.debug('bond_complexes : component {} carries {} elemental states...'.format(comp, len(bonds)))
        cur_component = (BondComplex({comp}, {state: val for state, val in zip(bonds, combi)},
                                     set(), set())
                         for combi in product((True, False), repeat=len(bonds)))
        single_components |= {bc for bc in cur_component if bc.is_consistent()}

    complexes = {bc for bc in single_components
                 if bc.contains_first_reactant() or bc.contains_second_reactant()}

    LOGGER.debug('bond_complexes {} : built {} single-molecule bond configurations, {} on reactants'
                 .format(datetime.now(), len(single_components), len(complexes)))
    LOGGER.debug('bond_complexes {} : building complexes'.format(datetime.now()))
    finished = False
    while not finished:
        LOGGER.debug('num of complexes {}'.format(len(complexes)))
        finished = True
        new_complexes = set()
        for (comp, mol) in product(complexes, single_components):
            if not comp.has_been_combined_with(mol) and not comp.contains(mol) and comp.can_connect_with(mol):
                new_complexes.add(comp.combined_with(mol))
                finished = False
        complexes |= new_complexes

    LOGGER.debug('bond_complexes {} : built {} complexes (including non fully-connected)'
                 .format(datetime.now(), len(complexes)))
    complexes = {bc for bc in complexes if bc.is_connected()}
    LOGGER.debug('bond_complexes {} : built {} complexes (only fully-connected)'.format(datetime.now(), len(complexes)))

    both_reactants = [c for c in complexes if c.contains_first_reactant() and c.contains_second_reactant()]
    first_reactant = [c for c in complexes if c.contains_first_reactant() and not c.contains_second_reactant()]
    second_reactant = [c for c in complexes if not c.contains_first_reactant() and c.contains_second_reactant()]

    LOGGER.debug('bond_complexes {} : combining complexes'.format(datetime.now()))
    if first_reactant and second_reactant:
        all_complexes = [c1.combined_with(c2) for c1 in first_reactant for c2 in second_reactant] + both_reactants
    elif first_reactant and not second_reactant:
        all_complexes = first_reactant + both_reactants
    else:
        all_complexes = second_reactant + both_reactants

    LOGGER.debug('bond_complexes {} : found total of {} complexes connected to either reactant'
                 .format(datetime.now(), len(all_complexes)))

    return all_complexes class BondComplex:
    """BondComplex is used to enumerate the different complexes that can be constructed.
    Logically this object is immutable, it is just the "caches" cannot_connect_with
    and already_combined_with that change, but these do not change the hash value."""

    def __init__(self, components: Set[Spec], states: Dict[State, bool], connected_bonds: Set[State],
                 contained_complexes: Set['BondComplex']) -> None:
        # These values determine the object's hash and are immutable.
        self.components = components
        self.states = states
        self.connected_bonds = connected_bonds
        self.contained_complexes = contained_complexes
        self._hash_value = hash(frozenset(self.states.items()))

        #  These values are mutated as we run through the building algo.
        self.cannot_connect_with = set()  # type: Set[BondComplex]
        self.already_combined_with = set()  # type: Set[BondComplex]

    def __eq__(self, other):
        return self.components == other.components and self.states == other.states \
               and self.connected_bonds == other.connected_bonds

    def __hash__(self):
        return self._hash_value

    def __str__(self) -> str:
        return 'BondComplex(C: {} ; T: {} ; F: {} ; D: {})' \
            .format(', '.join(str(x) for x in self.components),
                    ', '.join(str(x) for x in self.true_states()),
                    ', '.join(str(x) for x in self.false_states()),
                    ', '.join(str(x) for x in self.dangling_bonds()))

    def contains(self, other: 'BondComplex') -> bool:
        return other in self.contained_complexes

    def true_states(self) -> Set[State]:
        return {state for state, val in self.states.items() if val}

    def false_states(self) -> Set[State]:
        return {state for state, val in self.states.items() if not val}

    def dangling_bonds(self) -> Set[State]:
        return {s for s, v in self.states.items() if isinstance(s, InteractionState)
                and v and s not in self.connected_bonds}

    def can_connect_with(self, other: 'BondComplex') -> bool:
        if other in self.cannot_connect_with:
            return False
        elif not self.components.intersection(other.components) and \
                self.dangling_bonds().intersection(other.dangling_bonds()):
            return True
        else:
            self.cannot_connect_with.add(other)
            other.cannot_connect_with.add(self)
            return False

    def has_been_combined_with(self, other: 'BondComplex') -> bool:
        return other in self.already_combined_with

    def combined_with(self, other: 'BondComplex') -> 'BondComplex':
        cx = BondComplex(self.components | other.components,
                         {**self.states, **other.states},
                         self.connected_bonds | other.connected_bonds | (
                         self.dangling_bonds() & other.dangling_bonds()),
                         self.contained_complexes | other.contained_complexes | {self, other})

        self.already_combined_with.add(other)
        other.already_combined_with.add(self)

        return cx

    def contains_first_reactant(self) -> bool:
        return 0 in (c.struct_index for c in self.components)

    def contains_second_reactant(self) -> bool:
        return 1 in (c.struct_index for c in self.components)

    def is_valid(self) -> bool:
        return self.is_connected() and self.is_consistent()

    def is_connected(self) -> bool:
        return not self.dangling_bonds()

    def is_consistent(self) -> bool:
        # No two mutually exclusive states can be true at the same time.
        for state, val in self.states.items():
            if val and any(state.is_mutually_exclusive_with(other)
                           for other, other_val in self.states.items() if other_val):
                return False
        return True

    def to_venn_set(self) -> VennSet[State]:
        return Intersection(
            *(ValueSet(s) for s in self.true_states()),
            *(Complement(ValueSet(s)) for s in self.false_states())
        ) class ValueSet(UnarySet[T_inv], Generic[T_inv]):
    def __init__(self, value: T_inv) -> None:
        assert value is not None
        assert isinstance(hash(value), int)
        self.value = value

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Set):
            return NotImplemented
        elif isinstance(other, ValueSet):
            return self.value == other.value
        else:
            return False

    def __hash__(self) -> int:
        return hash('*value-set-{}*'.format(hash(self.value)))

    def __repr__(self) -> str:
        return str(self)

    def __str__(self) -> str:
        if self.value:
            return '{}'.format(str(self.value))
        else:
            return 'UniversalSet'

    @property
    def values(self) -> List[T_inv]:
        return [self.value]

    def _to_pyeda_expr(self, val_to_sym: MutableMapping[Any, str]) -> Expression:
        return expr(val_to_sym[self.value]) class Union(NarySet[T], Generic[T]):
    def __new__(cls, *exprs: Set[T], **kwargs: Any) -> Set[T]:
        if len(exprs) == 0:
            return EmptySet()
        else:
            return NarySet.__new__(cls, *exprs)  # type: ignore

    def __deepcopy__(self, memodict: Dict) -> Set[T]:
        return Union(*(deepcopy(x) for x in self.exprs))

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Set):
            return NotImplemented
        elif isinstance(other, Union):
            return all(my_expr == other_expr for my_expr, other_expr in zip(self.exprs, other.exprs))
        else:
            return False

    def __hash__(self) -> int:
        return hash(str(self))

    def __repr__(self) -> str:
        return str(self)

    def __str__(self) -> str:
        return '({})'.format(' | '.join(str(expr) for expr in self.exprs))

    def _to_pyeda_expr(self, val_to_sym: MutableMapping[T, str]) -> Expression:
        return Or(*(expr._to_pyeda_expr(val_to_sym) for expr in self.exprs))
from itertools import combinations
from rxncon.core.spec import Spec, LocusResolution, MRNASpec, spec_from_str
from rxncon.core.state import state_from_str
from rxncon.input.quick.quick import Quick
from rxncon.simulation.rule_based.rule_based_model import complex_from_str, rule_from_str, rule_based_model_from_rxncon, \
    initial_condition_from_str, bond_complexes, BondComplex
from rxncon.venntastic.sets import ValueSet, Union
import pytest
import rxncon.core.reaction as reaction
def test_rule_from_str_inequivalent() -> None:
    rule_classes = [
        ['A(x) + A(y,r~p) -> A(x!1,r~p).A(y!1) kk', 'A(x) + A(y,r~p) -> A(y!1).A(x!1,r~p) kk']
    ]

    for rule_class in rule_classes:
        for first_rule, second_rule in combinations(rule_class, 2):
            assert rule_from_str(first_rule).is_equivalent_to(rule_from_str(second_rule))
            assert rule_from_str(second_rule).is_equivalent_to(rule_from_str(first_rule))


def test_bond_complex_class() -> None:
    first_complex = BondComplex({spec_from_str('A@0')}, {state_from_str('A@0_[C]--C@2_[A]'): True}, set(), set())
    second_complex = BondComplex({spec_from_str('C@2')}, {state_from_str('A@0_[C]--C@2_[A]'): True}, set(), set())

    assert first_complex.dangling_bonds() == {state_from_str('A@0_[C]--C@2_[A]')}
    assert second_complex.dangling_bonds() == {state_from_str('A@0_[C]--C@2_[A]')}
    assert first_complex.can_connect_with(second_complex)

    first_and_second = first_complex.combined_with(second_complex)
    assert first_complex.dangling_bonds() == {state_from_str('A@0_[C]--C@2_[A]')}
    assert second_complex.dangling_bonds() == {state_from_str('A@0_[C]--C@2_[A]')}
    assert first_and_second.is_connected()
    assert not first_and_second.dangling_bonds()


def test_bond_complexes_calculation() -> None:
    states = Union(*(ValueSet(state_from_str(x))
                     for x in ('A@0_[C]--C@2_[A]', 'C@2_[(r)]-{p}',
                               'A@0_[C]--0', 'C@2_[A]--0', 'C@2_[(r)]-{0}')))

    assert len(bond_complexes(states)) == 3


def test_bond_complexes_ring_calculation() -> None:
    states = Union(*(ValueSet(state_from_str(x))
                     for x in ('A@0_[B]--B@2_[A]', 'B@2_[C]--C@3_[B]',
                               'C@3_[D]--D@4_[C]', 'D@4_[A]--A@0_[D]')))

    assert len(bond_complexes(states)) == 11


def test_bond_complexes_poly_calculation() -> None:
    states = Union(*(ValueSet(state_from_str(x))
                     for x in ('R@1_[Y]--Y@2_[R]', 'Y@2_[Y]--Y@3_[Y]',
                               'Y@3_[R]--R@4_[Y]', 'R@4_[Ras]--Ras@5_[R]')))

    assert len(bond_complexes(states)) == 5


def test_bond_complexes_calculation_unordered() -> None:
    states = Union(*(ValueSet(state_from_str(x))
                     for x in ('A@0_[C]--C@5_[A]', 'C@5_[B]--B@3_[C]', 'B@3_[D]--D@2_[B]')))

    assert len(bond_complexes(states)) == 4


# Test simple rxncon systems.
def test_single_requirement_modification() -> None:
    rbm = rule_based_model_from_rxncon(Quick("""A_[b]_ppi+_B_[a]; ! A_[(r)]-{p}
                                             A_[b]_ppi-_B_[a]
                                             C_p+_A_[(r)]
                                             D_p-_A_[(r)]""").rxncon_system)

    expected_rules = [
        'A(rR~p,bD) + B(aD) -> A(rR~p,bD!1).B(aD!1) k',
        'A(bD!1).B(aD!1) -> A(bD) + B(aD) k',
        'C() + A(rR~0) -> C() + A(rR~p) k',
        'A(rR~p) + D() -> A(rR~0) + D() k'
    ]

    assert len(rbm.rules) == len(expected_rules)

    for actual_rule in rbm.rules:
        print(actual_rule)
        # assert any(rule_from_str(rule).is_equivalent_to(actual_rule) for rule in expected_rules)

    expected_ics = [
        'A(bD,rR~0) NumA',
        'B(aD) NumB',
        'D() NumD',
        'C() NumC'
    ]

    for actual_ic in rbm.initial_conditions:
        assert any(initial_condition_from_str(ic).is_equivalent_to(actual_ic) for ic in expected_ics)


def test_single_inhibition_modification() -> None:
    rbm = rule_based_model_from_rxncon(Quick("""A_[b]_ppi+_B_[a]; x A_[(r)]-{p}
                                             E_[x]_ppi+_B_[a]
                                             C_p+_A_[(r)]
                                             D_ub+_A_[(r)]""").rxncon_system)

    expected_rules = [
        'A(rR~ub,bD) + B(aD) -> A(rR~ub,bD!1).B(aD!1) k',
        'A(rR~0,bD) + B(aD) -> A(rR~0,bD!1).B(aD!1) k',
        'B(aD) + E(xD) -> B(aD!1).E(xD!1) k',
        'A(rR~0) + C() -> A(rR~p) + C() k',
        'A(rR~0) + D() -> A(rR~ub) + D() k'
    ]

    assert len(rbm.rules) == len(expected_rules)

    for actual_rule in rbm.rules:
        assert any(rule_from_str(rule).is_equivalent_to(actual_rule) for rule in expected_rules)

    expected_ics = [
        'A(bD,rR~0) NumA',
        'B(aD) NumB',
        'D() NumD',
        'C() NumC',
        'E(xD) NumE'
    ]

    for actual_ic in rbm.initial_conditions:
        assert any(initial_condition_from_str(ic).is_equivalent_to(actual_ic) for ic in expected_ics)


def test_single_inhibition_interaction() -> None:

    rbm = rule_based_model_from_rxncon(Quick("""A_[b]_ppi+_B_[a]; x A_[(r)]-{p}