class Effector(ABC):
    """Effector is the abstract parent class of the different types of Effector."""
    def __init__(self):
        """All children have the `name` attribute, this code will never be called but exists purely to
        satisy the type checker."""
        self.name = None  # type: Optional[str]

    @property
    @abstractmethod
    def states(self) -> List[State]:
        pass

    @property
    def is_structured(self) -> bool:
        raise NotImplementedError

    @property
    def equivs_specs(self) -> List[Spec]:
        return []

    def collect_global_equivs(self, glob_equivs: StructEquivalences=None,
                              counter: StructCounter=None,
                              cur_namespace: List[str]=None) -> Tuple[StructEquivalences, StructCounter]:
        """Recursively collects all StructEquivalences inside the Effector, without mutating anything."""
        raise NotImplementedError

    def to_global_struct_effector(self, glob_equivs: StructEquivalences=None,
                                  counter: StructCounter=None,
                                  cur_namespace: List[str]=None) -> 'Effector':
        """Returns an effector in which the Specs are recursively re-labelled using a StructEquivalences object."""
        raise NotImplementedError

    @staticmethod
    def _init_to_struct_effector_args(glob_equivs: Optional[StructEquivalences], cur_index: Optional[StructCounter],
                                      cur_namespace: Optional[List[str]]) \
            -> Tuple[StructEquivalences, StructCounter, List[str]]:
        if glob_equivs is None:
            glob_equivs = StructEquivalences()
        if cur_index is None:
            cur_index = StructCounter()
        if cur_namespace is None:
            cur_namespace = []

        return glob_equivs, cur_index, cur_namespace class StructEquivalences:
    """StructEquivalences holds equivalence classes of QualSpecs. Every element in an equivalence class corresponds
    to the same physical molecule. Elements within the same equivalence class might be labelled with a different
    structure index when living in different namespaces."""
    def __init__(self) -> None:
        self.eq_classes = []  # type: List[List[QualSpec]]

    def __str__(self) -> str:
        s = ''
        for num, eq_class in enumerate(self.eq_classes):
            s = s + 'EqClass {}: {}\n'.format(num, str(eq_class))

        return s

    @property
    def specs(self):
        return list(set(qual_spec.spec.to_component_spec().to_non_struct_spec()
                        for eq_class in self.eq_classes for qual_spec in eq_class))

    def add_equivalence(self, first_qual_spec: QualSpec, second_qual_spec: QualSpec) -> None:
        first_qual_spec, second_qual_spec = first_qual_spec.to_component_qual_spec(), \
                                            second_qual_spec.to_component_qual_spec()

        found_first = None
        found_second = None
        for eq_class in self.eq_classes:
            if first_qual_spec in eq_class:
                if second_qual_spec not in eq_class:
                    eq_class.append(second_qual_spec)
                found_first = eq_class
            elif second_qual_spec in eq_class:
                if first_qual_spec not in eq_class:
                    eq_class.append(first_qual_spec)
                found_second = eq_class

        if found_first and found_second:
            found_first += found_second
            self.eq_classes.remove(found_second)

        if not (found_first or found_second):
            self.eq_classes.append([first_qual_spec, second_qual_spec])

    def add_equivalence_class(self, eq_class: List[QualSpec]) -> None:
        for existing_class in self.eq_classes:
            if next((x for x in existing_class if x.to_component_qual_spec() in eq_class), False):
                for elem in eq_class:
                    self.add_equivalence(elem.to_component_qual_spec(), existing_class[0])
                return

        self.eq_classes.append([x.to_component_qual_spec() for x in eq_class])

    def merge_with(self, other: 'StructEquivalences', other_base_namespace: List[str]) -> None:
        for other_eq_class in other.eq_classes:
            self.add_equivalence_class([x.with_prepended_namespace(other_base_namespace) for x in other_eq_class])

    def find_unqualified_spec(self, qual_spec: QualSpec) -> Optional[Spec]:
        for eq_class in self.eq_classes:
            if qual_spec.to_component_qual_spec() in eq_class:
                existing_spec = deepcopy(
                    next((x.spec for x in eq_class if x.is_in_root_namespace), None))  # type: ignore
                if existing_spec:
                    existing_spec.locus = deepcopy(qual_spec.spec.locus)
                    return existing_spec

        return None

    def indices_in_root_namespace(self) -> List[int]:
        return [qspec.spec.struct_index for eq_class in self.eq_classes for qspec in eq_class
                if qspec.is_in_root_namespace and qspec.spec.struct_index is not None] class QualSpec:
    """QualSpec holds a Spec that lives in a namespace. The namespace is a list of
    (stringified) boolean contingency names."""
    def __init__(self, namespace: List[str], spec: Spec) -> None:
        self.namespace = namespace
        self.spec = spec
        self._name = '.'.join(namespace + [str(spec)])

    def __str__(self) -> str:
        return self._name

    def __repr__(self) -> str:
        return 'QualSpec<{}>'.format(self._name)

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, QualSpec):
            return NotImplemented
        return self.namespace == other.namespace and self.spec == other.spec

    def to_component_qual_spec(self) -> 'QualSpec':
        return QualSpec(self.namespace, self.spec.to_component_spec())

    @property
    def is_in_root_namespace(self) -> bool:
        return not self.namespace

    def with_prepended_namespace(self, extra_namespace: List[str]) -> 'QualSpec':
        new_namespace = deepcopy(extra_namespace) + deepcopy(self.namespace)
        return QualSpec(new_namespace, self.spec) class StateEffector(Effector):
    """StateEffector holds a State and therefore is a leaf in the Effector."""
    def __init__(self, expr: State, name: Optional[str]=None) -> None:
        self.expr = expr
        self.name = name

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

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

    def __str__(self) -> str:
        return 'StateEffector({})'.format(str(self.expr))

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Effector):
            return NotImplemented
        return isinstance(other, StateEffector) and self.expr == other.expr and self.name == other.name

    @property
    def states(self) -> List[State]:
        return [deepcopy(self.expr)]

    @property
    def is_structured(self) -> bool:
        return self.expr.is_structured

    def collect_global_equivs(self, glob_equivs: StructEquivalences = None,
                              counter: StructCounter = None,
                              cur_namespace: List[str] = None) -> Tuple[StructEquivalences, StructCounter]:
        glob_equivs, counter, cur_namespace = self._init_to_struct_effector_args(glob_equivs, counter, cur_namespace)
        return glob_equivs, counter

    def to_global_struct_effector(self, glob_equivs: StructEquivalences = None,
                                  counter: StructCounter = None,
                                  cur_namespace: List[str] = None) -> Effector:
        glob_equivs, counter, cur_namespace = self._init_to_struct_effector_args(glob_equivs, counter, cur_namespace)
        state = deepcopy(self.expr)
        updates = {}

        assert not (state.is_homodimer and not state.is_structured), \
            'Please provide structure annotation for homodimer {}'.format(state)

        LOGGER.debug('to_global_struct_effector : Merging {}'.format(str(self)))
        LOGGER.debug('to_global_struct_effector : Equivs {}'.format(glob_equivs))

        for spec in state.specs:
            existing_spec = glob_equivs.find_unqualified_spec(QualSpec(cur_namespace, spec))

            if existing_spec:
                updates[spec] = existing_spec
            else:
                new_spec = deepcopy(spec)
                new_spec.struct_index = self._generate_index(glob_equivs, counter)

                updates[spec] = new_spec
                glob_equivs.add_equivalence(QualSpec([], new_spec), QualSpec(cur_namespace, spec))

        state.update_specs(updates)
        LOGGER.debug('to_global_struct_effector : Result {}'.format(str(state)))

        return StateEffector(state, name=self.name)

    @staticmethod
    def _generate_index(glob_equivs: StructEquivalences, cur_index: StructCounter) -> int:
        index = cur_index.value
        while index in glob_equivs.indices_in_root_namespace():
            cur_index.increment()
            index = cur_index.value

        return index class TrivialStructEquivalences(StructEquivalences):
    """TrivialStructEquivalences describes a situation in which all molecules are inequivalent:
    all namespacing information is ignored."""
    def __init__(self, initial_struct_specs: Dict[Spec, Spec]=None) -> None:
        if not initial_struct_specs:
            self.struct_specs = {}  # type: Dict[Spec, Spec]
        else:
            self.struct_specs = initial_struct_specs

        self.cur_index = 2

    def __str__(self) -> str:
        return 'TrivialStructEquivalences'

    @property
    def specs(self):
        return list(set(spec.to_component_spec().to_non_struct_spec() for spec in self.struct_specs.keys()))

    def add_equivalence(self, first_qual_spec: QualSpec, second_qual_spec: QualSpec) -> None:
        pass

    def add_equivalence_class(self, eq_class: List[QualSpec]) -> None:
        pass

    def merge_with(self, other: 'StructEquivalences', other_base_namespace: List[str]) -> None:
        pass

    def find_unqualified_spec(self, qual_spec: QualSpec) -> Optional[Spec]:
        try:
            struct_spec = deepcopy(self.struct_specs[qual_spec.spec.to_component_spec()])
            struct_spec.locus = deepcopy(qual_spec.spec.locus)
            return struct_spec
        except KeyError:
            self.struct_specs[qual_spec.spec.to_component_spec()] = \
                deepcopy(qual_spec.spec.to_component_spec().with_struct_index(self.cur_index))
            self.cur_index += 1
            return self.find_unqualified_spec(qual_spec)

    def indices_in_root_namespace(self) -> List[int]:
        return [x for x in range(self.cur_index)] class NotEffector(Effector):
    """NotEffector holds another Effector object, and possibly a name, which it can inherit from the
    Boolean Contingency from which it was constructed."""
    def __init__(self, expr: Effector, **kwargs: Optional[str]) -> None:
        try:
            self.name = kwargs['name']
        except KeyError:
            self.name = None
        self.expr = expr

    def __str__(self) -> str:
        return 'NotEffector({})'.format(self.expr)

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Effector):
            return NotImplemented
        return isinstance(other, NotEffector) and self.expr == other.expr and self.name == other.name

    @property
    def states(self) -> List[State]:
        return self.expr.states

    @property
    def is_structured(self) -> bool:
        return self.expr.is_structured

    @property
    def equivs_specs(self) -> List[Spec]:
        return self.expr.equivs_specs

    def collect_global_equivs(self, glob_equivs: StructEquivalences = None,
                              counter: StructCounter = None,
                              cur_namespace: List[str] = None) -> Tuple[StructEquivalences, StructCounter]:
        if not self.name:
            raise AssertionError('Cannot collect_global_equivs nameless NotEffectors.')

        glob_equivs, counter, cur_namespace = self._init_to_struct_effector_args(glob_equivs, counter, cur_namespace)
        return self.expr.collect_global_equivs(glob_equivs, counter, cur_namespace + [self.name])

    def to_global_struct_effector(self, glob_equivs: StructEquivalences = None,
                                  counter: StructCounter = None,
                                  cur_namespace: List[str] = None) -> Effector:
        if not self.name:
            raise AssertionError('Cannot to_global_struct_effector nameless NotEffectors.')

        glob_equivs, counter, cur_namespace = self._init_to_struct_effector_args(glob_equivs, counter, cur_namespace)
        return NotEffector(self.expr.to_global_struct_effector(glob_equivs, counter, cur_namespace + [self.name]),
                           name=self.name) class AndEffector(NaryEffector):
    """AndEffector describes a logical AND between two or more Effectors. If the AndEffector
    contains only a single effector, we replace the AndEffector's construction with that
    single effector."""
    def __deepcopy__(self, memodict: Dict) -> 'AndEffector':
        """It is required to override __deepcopy__ since we are overriding __new__ with the
        functionality to return an object of a different class."""
        res = AndEffector(*(deepcopy(x) for x in self.exprs))
        res.name = self.name
        res.equivs = deepcopy(self.equivs)

        return res

    def __str__(self) -> str:
        if self.name:
            return 'AndEffector{0}({1})'.format(self.name, ','.join(str(x) for x in self.exprs))
        else:
            return 'AndEffector({0})'.format(','.join(str(x) for x in self.exprs))

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

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Effector):
            return NotImplemented
        return isinstance(other, AndEffector) and self.name == other.name and self.exprs == other.exprs class OrEffector(NaryEffector):
    """OrEffector describes a logical OR between two or more Effectors. If the OrEffector
    contains only a single effector, we replace the OrEffector's construction with that
    single effector."""
    def __deepcopy__(self, memodict: Dict) -> 'OrEffector':
        """It is required to override __deepcopy__ since we are overriding __new__ with the
        functionality to return an object of a different class."""
        res = OrEffector(*(deepcopy(x) for x in self.exprs))
        res.name = self.name
        res.equivs = deepcopy(self.equivs)

        return res

    def __str__(self) -> str:
        if self.name:
            return 'OrEffector{0}({1})'.format(self.name, ','.join(str(x) for x in self.exprs))
        else:
            return 'OrEffector({0})'.format(','.join(str(x) for x in self.exprs))

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

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Effector):
            return NotImplemented
        return isinstance(other, OrEffector) and self.name == other.name and self.exprs == other.exprs class StructCounter:
    """StructCounter is a helper class that generates structure indices. By default it starts counting at 2 since the
    numbers 0 and 1 are reserved for the reactants."""
    def __init__(self, counter_start: Optional[int]=2) -> None:
        if counter_start is None:
            counter_start = 2
        self.value = counter_start

    def increment(self) -> None:
        self.value += 1 class Reaction:  # pylint: disable=too-many-instance-attributes
    """Generic Reaction class. A Reaction is basically a unidirectional rule with terms on the left hand side
    and terms on the right hand side, which is defined by its ReactionDef. A State is 'consumed' if it is on
    the LHS, but not on the RHS (and its component does not vanish); it is 'produced' if it is on the RHS but
    not on the LHS (and its component does not vanish); it is degraded if its component vanishes going from
    LHS to RHS; it is synthesised if its component appears going from LHS to RHS."""
    def __init__(self, reaction_def: ReactionDef, var_to_val: Dict[str, Any]) -> None:
        self.reaction_class = reaction_def.reaction_class
        self.reaction_def = reaction_def
        self.var_to_val = var_to_val

        self.terms_lhs = reaction_def.terms_lhs_from_vars(var_to_val)
        self.terms_rhs = reaction_def.terms_rhs_from_vars(var_to_val)
        self.name = reaction_def.name_from_vars(var_to_val)

        # This information is cached, and lazily initialised.
        self._consumed_states = None  # type: Optional[List[State]]
        self._produced_states = None  # type: Optional[List[State]]
        self._synthesised_states = None  # type: Optional[List[State]]
        self._degraded_states = None  # type: Optional[List[State]]

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

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

    def __str__(self) -> str:
        return self.name

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Reaction):
            return NotImplemented
        return self.reaction_def == other.reaction_def and self.var_to_val == other.var_to_val

    def __getitem__(self, item: str) -> Any:
        return self.var_to_val[item]

    def invalidate_state_cache(self) -> None:
        self._consumed_states = None
        self._produced_states = None
        self._synthesised_states = None
        self._degraded_states = None

    @property
    def components_lhs(self) -> List[Spec]:
        return [spec for term in self.terms_lhs for spec in term.specs]

    @property
    def components_rhs(self) -> List[Spec]:
        return [spec for term in self.terms_rhs for spec in term.specs]

    @property
    def components(self) -> List[Spec]:
        return list(set(self.components_lhs + self.components_rhs))

    @property
    def components_lhs_structured(self) -> List[Spec]:
        return [spec.with_struct_index(i) for i, spec in enumerate(self.components_lhs)]

    @property
    def components_rhs_structured(self) -> List[Spec]:
        return [spec.with_struct_index(i) for i, spec in enumerate(self.components_rhs)]

    @property
    def consumed_states(self) -> List[State]:
        if self._consumed_states is None:
            self._consumed_states = []

            for term in self.terms_lhs:
                if all(spec in self.components_rhs for spec in term.specs):
                    self._consumed_states += term.states

        return self._consumed_states

    @property
    def produced_states(self) -> List[State]:
        if self._produced_states is None:
            self._produced_states = []

            for term in self.terms_rhs:
                if all(spec in self.components_lhs for spec in term.specs):
                    self._produced_states += term.states

        return self._produced_states

    @property
    def degraded_states(self) -> List[State]:
        if self._degraded_states is None:
            self._degraded_states = []

            for term in self.terms_lhs:
                if not any(spec in self.components_rhs for spec in term.specs):
                    self._degraded_states += term.states

        return self._degraded_states

    @property
    def synthesised_states(self) -> List[State]:
        if self._synthesised_states is None:
            self._synthesised_states = []

            for term in self.terms_rhs:
                if not any(spec in self.components_lhs for spec in term.specs):
                    # if none of the components from the lhs are in termn_rhs do:
                    self._synthesised_states += term.states


        return self._synthesised_states

    @property
    def degraded_components(self) -> List[Spec]:
        structured_components = [component for component in self.components_lhs_structured if component not in self.components_rhs_structured]
        components = []
        for component in structured_components:
            # destructurize
            component.struct_index = None
            components.append(component)
        return components

    @property
    def synthesised_components(self) -> List[Spec]:
        structured_components= [component for component in self.components_rhs_structured if component not in self.components_lhs_structured]
        components = []
        for component in structured_components:
            # destructurize
            component.struct_index = None
            components.append(component)
        return components

    @property
    def modifier_components(self) -> List[Spec]:
        res = []  # type: List[Spec]
        for term in self.terms_lhs:
            if term in self.terms_rhs:
                res += term.specs

        return res

    @property
    def modifier_states(self) -> List[State]:
        res = []  # type: List[State]
        for term in self.terms_lhs:
            if term in self.terms_rhs:
                res += term.states

        return res class OutputReaction(Reaction):
    """OutputReactions do not have an underlying rule: they typically describe something like a change
    in a global quantity; therefore they cannot be described by the generic Reaction class."""
    def __init__(self, name: str) -> None:  # pylint: disable=super-init-not-called
        self.name = name
        self.terms_lhs = []
        self.terms_rhs = []

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

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Reaction):
            return NotImplemented
        return isinstance(other, OutputReaction) and self.name == other.name

    def __str__(self) -> str:
        return self.name

    def __repr__(self) -> str:
        return 'OutputReaction<{}>'.format(self.name)

    @property
    def components_rhs(self) -> List[Spec]:
        return []

    @property
    def consumed_states(self) -> List[State]:
        return []

    @property
    def degraded_states(self) -> List[State]:
        return []

    @property
    def components_lhs(self) -> List[Spec]:
        return []

    @property
    def synthesised_states(self) -> List[State]:
        return []

    @property
    def degraded_components(self) -> List[Spec]:
        return []

    @property
    def synthesised_components(self) -> List[Spec]:
        return []

    @property
    def produced_states(self) -> List[State]:
        return []

    def invalidate_state_cache(self) -> None:
        pass class Set(Generic[T]):
    def calc_solutions(self) -> List[Dict[T, bool]]:
        val_to_sym = self._make_val_to_sym_dict()
        sym_to_val = {sym: val for val, sym in val_to_sym.items()}

        venn_solns = []
        for s in self._to_pyeda_expr(val_to_sym).satisfy_all():
            venn_solns.append({sym_to_val[sym.name]: bool(truth) for sym, truth in s.items()})

        return venn_solns

    def eval_boolean_func(self, vars: Dict[T, bool]) -> bool:
        val_to_sym = self._make_val_to_sym_dict()
        try:
            evaluated = self._to_pyeda_expr(val_to_sym).restrict({expr(sym): vars[val] for val, sym in val_to_sym.items()})
        except KeyError as e:
            raise AssertionError('eval_boolean_func missing variable {}'.format(e.args[0]))

        if evaluated.is_one():
            return True
        elif evaluated.is_zero():
            return False
        else:
            raise AssertionError

    def to_simplified_set(self) -> 'Set[T]':
        val_to_sym = self._make_val_to_sym_dict()
        sym_to_val = {sym: val for val, sym in val_to_sym.items()}
        return venn_from_pyeda(self._to_pyeda_expr(val_to_sym).simplify(), sym_to_val)

    def to_dnf_set(self) -> 'Set[T]':
        val_to_sym = self._make_val_to_sym_dict()
        sym_to_val = {sym: val for val, sym in val_to_sym.items()}
        return venn_from_pyeda(self._to_pyeda_expr(val_to_sym).to_dnf(), sym_to_val)

    def to_dnf_list(self) -> List['Set[T]']:
        val_to_sym = self._make_val_to_sym_dict()
        sym_to_val = {sym: val for val, sym in val_to_sym.items()}
        dnf_set = self._to_pyeda_expr(val_to_sym).to_dnf()

        if dnf_set is One:
            return [UniversalSet()]
        elif dnf_set is Zero:
            return [EmptySet()]
        elif isinstance(dnf_set, Literal):
            return [venn_from_pyeda(dnf_set, sym_to_val)]
        elif isinstance(dnf_set, AndOp):
            return [venn_from_pyeda(dnf_set, sym_to_val)]
        elif isinstance(dnf_set, OrOp):
            return [venn_from_pyeda(term, sym_to_val) for term in dnf_set.xs]
        else:
            raise Exception

    def to_dnf_nested_list(self) -> List[List['Set[T]']]:
        val_to_sym = self._make_val_to_sym_dict()
        sym_to_val = {sym: val for val, sym in val_to_sym.items()}
        dnf_set = self._to_pyeda_expr(val_to_sym).to_dnf()

        if dnf_set is One:
            return [[UniversalSet()]]
        elif dnf_set is Zero:
            return [[EmptySet()]]
        elif isinstance(dnf_set, Literal):
            return [[venn_from_pyeda(dnf_set, sym_to_val)]]
        elif isinstance(dnf_set, AndOp):
            return [[venn_from_pyeda(x, sym_to_val) for x in dnf_set.xs]]

        res = []  # type: List[List[Set[T]]]
        for term in dnf_set.xs:
            if isinstance(term, Literal):
                res.append([venn_from_pyeda(term, sym_to_val)])
            elif isinstance(term, AndOp):
                res.append([venn_from_pyeda(x, sym_to_val) for x in term.xs])
            else:
                raise Exception
        return res

    def is_equivalent_to(self, other: 'Set[T]') -> bool:
        val_to_sym = self._make_val_to_sym_dict()
        val_to_sym = other._make_val_to_sym_dict(val_to_sym)
        return self._to_pyeda_expr(val_to_sym).equivalent(other._to_pyeda_expr(val_to_sym))

    def is_subset_of(self, other: 'Set[T]') -> bool:
        val_to_sym = self._make_val_to_sym_dict()
        val_to_sym = other._make_val_to_sym_dict(val_to_sym)
        return Implies(self._to_pyeda_expr(val_to_sym), other._to_pyeda_expr(val_to_sym)).equivalent(expr(1))

    def is_superset_of(self, other: 'Set[T]') -> bool:
        return other.is_subset_of(self)

    @property
    def values(self) -> List[T]:
        return []

    def _to_pyeda_expr(self, val_to_sym: MutableMapping[T, str]) -> Expression:
        return None

    def _make_val_to_sym_dict(self, existing_dict: Optional[MutableMapping[T, str]]=None) -> MutableMapping[T, str]:
        vals = []  # type: List[T]

        for v in self.values:
            found = False
            for existing in vals:
                try:
                    if v == existing:
                        found = True
                        break
                except AttributeError:
                    pass

            if not found:
                vals.append(v)

        if existing_dict:
            d = existing_dict
            first_new_sym_idx = SYMS.index(next(reversed(d.values()))) + 1  # type: ignore
            for i, val in enumerate(x for x in vals if x not in d.keys()):
                d[val] = SYMS[i + first_new_sym_idx]
        else:
            d = OrderedDict()
            for i, val in enumerate(vals):
                d[val] = SYMS[i]

        return d 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 Intersection(NarySet[T], Generic[T]):
    def __new__(cls, *exprs: Set[T], **kwargs: Any) -> Set[T]:
        if len(exprs) == 0:
            return UniversalSet()
        else:
            return NarySet.__new__(cls, *exprs)  # type: ignore

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

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Set):
            return NotImplemented
        elif isinstance(other, Intersection):
            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 And(*(expr._to_pyeda_expr(val_to_sym) for expr in self.exprs)) class Complement(UnarySet[T], Generic[T]):
    def __init__(self, expr: Set[T]) -> None:
        self.expr = expr

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

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

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

    def __str__(self) -> str:
        return '!({})'.format(str(self.expr))

    @property
    def values(self) -> List[T]:
        return self.expr.values

    def _to_pyeda_expr(self, val_to_sym: MutableMapping[T, str]) -> Expression:
        return Not(self.expr._to_pyeda_expr(val_to_sym)) 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)) class UniversalSet(Set[Any]):
    def __init__(self) -> None:
        pass

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Set):
            return NotImplemented
        return isinstance(other, UniversalSet)

    def __hash__(self) -> int:
        return hash('UniversalSet')

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

    def __str__(self) -> str:
        return 'UniversalSet'

    def _to_pyeda_expr(self, val_to_sym: MutableMapping[Any, str]) -> Expression:
        return expr(1) class State(metaclass=ABCMeta):
    """Abstract Base Class for all States."""
    def __eq__(self, other: object) -> bool:
        if not isinstance(other, State):
            return NotImplemented
        else:
            raise NotImplementedError

    def __lt__(self, other: object) -> bool:
        if not isinstance(other, State):
            return NotImplemented
        else:
            raise NotImplementedError

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

    @property
    @abstractmethod
    def is_elemental(self) -> bool:
        return False

    @abstractmethod
    def is_superset_of(self, other: 'State') -> bool:
        return False

    @abstractmethod
    def is_subset_of(self, other: 'State') -> bool:
        return False

    @property
    def is_structured(self) -> bool:
        return all(spec.is_structured for spec in self.specs)

    @property
    def is_global(self) -> bool:
        return False

    @abstractmethod
    def is_mutually_exclusive_with(self, state: 'State') -> bool:
        return False

    @property
    @abstractmethod
    def is_neutral(self) -> bool:
        return False

    @property
    @abstractmethod
    def is_homodimer(self) -> bool:
        return False

    @property
    @abstractmethod
    def neutral_states(self) -> List['State']:
        return []

    @property
    @abstractmethod
    def specs(self) -> List[Spec]:
        return []

    @abstractmethod
    def update_specs(self, updates: Dict[Spec, Spec]) -> None:
        pass

    @abstractmethod
    def to_non_structured(self) -> 'State':
        pass

    @abstractmethod
    def to_structured_from_spec(self, spec: Spec) -> 'State':
        pass

    def to_structured_from_state(self, state: 'State') -> 'State':
        structured = self.clone()
        for spec in state.specs:
            structured = structured.to_structured_from_spec(spec)

        return structured

    @property
    @abstractmethod
    def components(self) -> List[Spec]:
        pass
from enum import unique, Enum
from copy import deepcopy
from collections import defaultdict
from typing import Any, Callable, Set as TgSet, Dict, List, Optional
from rxncon.core.effector import Effector, StructEquivalences, QualSpec, StateEffector, TrivialStructEquivalences, \
    NotEffector, AndEffector, OrEffector, StructCounter
from rxncon.core.reaction import Reaction, OutputReaction
from rxncon.venntastic.sets import Set as VennSet, ValueSet, Intersection, Complement, Union, UniversalSet
from rxncon.core.state import State
import logging
"""Module containing the classes Contingency and ContingencyType."""





LOGGER = logging.getLogger(__name__)


@unique
class ContingencyType(Enum):
    """The ContingencyTypes requirement, inhibition are known as `strict` contingencies, whereas the
    positive, negative ContingencyTypes are referred to as quantitative."""
    requirement = '!'
    inhibition = 'x'
    positive = 'k+'
    negative = 'k-'
    no_effect = '0'
    unknown = '?'


class Contingency:
    """Contingency holds the triple `reaction`, `type`, `effector` describing a contingency in a rxncon model.
    Contingency objects are constructed from ContingencyListEntry objects, that live in the module
    rxncon.input.shared.contingency_list."""
    def __init__(self, reaction: Reaction, contingency_type: ContingencyType,

                 effector: Effector, validate_equivs_specs: bool=True) -> None: