def wrapper( factor: Factor, changes: Optional[Union[Sequence[Factor], ContextRegister]], context_opinion: Optional[Opinion] = None, ) -> Factor: if changes is None: return factor if not isinstance(changes, Iterable): changes = (changes, ) if not isinstance(changes, dict): generic_factors = factor.generic_factors if len(generic_factors) < len(changes): raise ValueError( f"The iterable {changes} is too long to be interpreted " + f"as a list of replacements for the " + f"{len(generic_factors)} items of generic_factors.") changes = ContextRegister(dict(zip(generic_factors, changes))) expanded_changes = ContextRegister({ seek_factor_by_name(old, factor, context_opinion): seek_factor_by_name(new, factor, context_opinion) for old, new in changes.items() }) for old, new in expanded_changes.items(): if factor.means(old) and factor.name == old.name: return new return func(factor, expanded_changes)
def _update_context_from_factors( self, other: Comparable, context: ContextRegister) -> Optional[ContextRegister]: incoming = ContextRegister( dict(zip(self.generic_factors, other.generic_factors))) updated_context = context.merged_with(incoming) return updated_context
def test_import_to_mapping_no_change(self, make_entity): old_mapping = ContextRegister( {make_entity["motel"]: make_entity["trees"]}) assert dict( old_mapping.merged_with( {make_entity["motel"]: make_entity["trees"]})) == { make_entity["motel"]: make_entity["trees"], }
def test_import_to_context_register(self, make_entity, watt_factor): f = watt_factor["f7"] left = ContextRegister({ watt_factor["f7"]: watt_factor["f7_swap_entities"], make_entity["motel"]: make_entity["trees"], }) right = ContextRegister({make_entity["trees"]: make_entity["motel"]}) assert len(left.merged_with(right)) == 3
def explanations_contradiction( self, other: Factor, context: ContextRegister = None ) -> Iterator[Explanation]: r""" Find context matches that would result in a contradiction with other. Works by testing whether ``self`` would imply ``other`` if ``other`` had an opposite value for ``rule_valid``. This method takes three main paths depending on whether the holdings ``self`` and ``other`` assert that rules are decided or undecided. A ``decided`` :class:`Rule` can never contradict a previous statement that any :class:`Rule` was undecided. If rule A implies rule B, then a holding that B is undecided contradicts a prior :class:`Rule` deciding that rule A is valid or invalid. :param other: The :class:`.Factor` to be compared to self. Unlike with :meth:`~Holding.contradicts`\, this method cannot be called with an :class:`.Opinion` for `other`. :returns: a generator yielding :class:`.ContextRegister`\s that cause a contradiction. """ if context is None: context = ContextRegister() if isinstance(other, Procedure): other = Rule(procedure=other) if isinstance(other, Rule): other = Holding(rule=other) if isinstance(other, self.__class__): yield from self._explanations_contradiction_of_holding(other, context) elif isinstance(other, Factor): yield from [] # no possible contradiction elif hasattr(other, "explanations_contradiction"): if context: context = context.reversed() yield from other.explanations_contradiction(self, context=context) else: raise TypeError( f"'Contradicts' test not implemented for types " f"{self.__class__} and {other.__class__}." )
def triggers_next_procedure_if_universal( self, other: Procedure, context: Optional[ContextRegister] = None ) -> Iterator[ContextRegister]: r""" Test if Factors from firing `self` trigger `other` if both are universal. The difference from :func:`triggers_next_procedure` is that this function doesn't require the "despite" :class:`.Factor`\s to be addressed. If both calling :class:`.Rules`\s apply in "ALL" cases where their inputs are present, then it doesn't matter what Factors they apply "despite". :param other: another :class:`Procedure` to test to see whether it can be triggered by triggering ``self`` :returns: whether the set of :class:`Factor`\s that exist after ``self`` is fired could trigger ``other`` """ context = context or ContextRegister() self_output_or_input = FactorGroup((*self.outputs, *self.inputs)) yield from self_output_or_input.comparison( operation=operator.ge, still_need_matches=list(other.inputs), matches=context, )
def explanations_implication(self, other: Comparable, context: Optional[ContextRegister] = None ) -> Iterator[ContextRegister]: r""" Generate :class:`.ContextRegister`\s that cause `self` to imply `other`. If self is `absent`, then generate a ContextRegister from other's point of view and then swap the keys and values. """ if context is None: context = ContextRegister() if not isinstance(other, Factor): raise TypeError( f"{self.__class__} objects may only be compared for " + "implication with other Factor objects or None.") if isinstance(other, self.__class__): if not self.__dict__.get("absent"): if not other.__dict__.get("absent"): yield from self._implies_if_present(other, context) else: yield from self._contradicts_if_present(other, context) else: if other.__dict__.get("absent"): test = other._implies_if_present(self, context.reversed()) else: test = other._contradicts_if_present( self, context.reversed()) yield from (register.reversed() for register in test)
def explanations_contradiction(self, other: Comparable, context: Optional[ContextRegister] = None ) -> Iterator[ContextRegister]: """ Test whether ``self`` :meth:`implies` the absence of ``other``. This should only be called after confirming that ``other`` is not ``None``. :returns: ``True`` if self and other can't both be true at the same time. Otherwise returns ``False``. """ if context is None: context = ContextRegister() if not isinstance(other, Factor): raise TypeError( f"{self.__class__} objects may only be compared for " + "contradiction with other Factor objects or None.") if isinstance(other, self.__class__): if not self.__dict__.get("absent"): if not other.__dict__.get("absent"): yield from self._contradicts_if_present(other, context) else: yield from self._implies_if_present(other, context) elif self.__dict__.get("absent"): if not other.__dict__.get("absent"): test = other._implies_if_present(self, context.reversed()) else: test = other._contradicts_if_present( self, context.reversed()) yield from (register.reversed() for register in test)
def implies( self, other: Optional[Comparable], context: ContextRegister = None ) -> bool: r""" Test for implication. See :meth:`.Procedure.implies_all_to_all` and :meth:`.Procedure.implies_all_to_some` for explanations of how ``inputs``, ``outputs``, and ``despite`` :class:`.Factor`\s affect implication. :param other: A :class:`Holding` to compare to self, or a :class:`.Rule` to convert into such a :class:`Holding` and then compare :returns: whether ``self`` implies ``other`` """ if other is None: return True if isinstance(other, (Rule, Procedure)): other = Holding(rule=other) if not isinstance(other, self.__class__): if hasattr(other, "implied_by"): if context: context = context.reversed() return other.implied_by(self, context=context) return False return any( explanation is not None for explanation in self.explanations_implication(other, context) )
def _union_if_not_exclusive( self, other: Holding, context: ContextRegister ) -> Optional[Holding]: if self.decided is other.decided is False: if self.rule.implies(other.rule, context=context): return other if other.rule.implies(self.rule, context=context.reversed()): return self return None if not self.decided or not other.decided: return None if self.rule_valid != other.rule_valid: return None if self.rule_valid is False: # If a Rule with input A present is not valid # and a Rule with input A absent is also not valid # then a version of the Rule with input A # omitted is also not valid. raise NotImplementedError( "The union operation is not yet implemented for Holdings " "that assert a Rule is not valid." ) new_rule = self.rule.union(other.rule, context=context) if not new_rule: return None return self.evolve({"rule": new_rule, "exclusive": False})
def ordered_comparison( self, other: FactorSequence, operation: Callable, context: Optional[ContextRegister] = None, ) -> Iterator[ContextRegister]: r""" Find ways for a series of pairs of :class:`.Factor`\s to satisfy a comparison. :param context: keys representing :class:`.Factor`\s in ``self`` and values representing :class:`.Factor`\s in ``other``. The keys and values have been found in corresponding positions in ``self`` and ``other``. :yields: every way that ``matches`` can be updated to be consistent with each element of ``self.need_matches`` having the relationship ``self.comparison`` with the item at the corresponding index of ``self.available``. """ def update_register( register: ContextRegister, factor_pairs: List[Tuple[Optional[Comparable], Optional[Comparable]]], i: int = 0, ): """ Recursively search through :class:`Factor` pairs trying out context assignments. This has the potential to take a long time to fail if the problem is unsatisfiable. It will reduce risk to check that every :class:`Factor` pair is satisfiable before checking that they're all satisfiable together. """ if i == len(factor_pairs): yield register else: left, right = factor_pairs[i] if left is not None or right is None: if left is None: yield from update_register(register, factor_pairs=factor_pairs, i=i + 1) else: new_mapping_choices: List[ContextRegister] = [] for incoming_register in left.update_context_register( right, register, operation): if incoming_register not in new_mapping_choices: new_mapping_choices.append(incoming_register) yield from update_register( incoming_register, factor_pairs=factor_pairs, i=i + 1, ) if context is None: context = ContextRegister() ordered_pairs = list(zip_longest(self, other)) yield from update_register(register=context, factor_pairs=ordered_pairs)
def contradicts( self, other: ComparableGroup, context: Optional[ContextRegister] = None, ) -> bool: r""" Find whether two sets of :class:`.Factor`\s can be contradictory. :param other: a second set of :class:`Factor`\s with context factors that are internally consistent, but may not be consistent with ``self_factors``. :param context: correspondences between :class:`Factor`s in self and other that can't be changed in seeking a contradiction :returns: whether any :class:`.Factor` assignment can be found that makes a :class:`.Factor` in the output of ``other`` contradict a :class:`.Factor` in the output of ``self``. """ if context is None: context = ContextRegister() for other_factor in other: for self_factor in self: if self_factor.contradicts(other_factor, context): return True return False
def explanations_same_meaning(self, other: Comparable, context: Optional[ContextRegister] = None ) -> Iterator[ContextRegister]: """Yield contexts that could cause self to have the same meaning as other.""" context = context or ContextRegister() if isinstance(other, self.__class__): yield from self._explanations_same_meaning_as_procedure( other, context)
def likely_contexts(self, other: Comparable, context: Optional[ContextRegister] = None ) -> Iterator[ContextRegister]: context = context or ContextRegister() if isinstance(other, Factor): yield from self._likely_contexts_for_factor(other, context) else: yield from self._likely_contexts_for_factorgroup(other, context)
def _likely_context_from_implication( self, other: Comparable, context: ContextRegister) -> Optional[ContextRegister]: new_context = None if self.implies(other, context=context) or other.implies( self, context=context.reversed()): new_context = self._update_context_from_factors(other, context) if new_context and new_context != context: return new_context return None
def explain_implication_all_to_some( self, other: Factor, context: Optional[ContextRegister] = None ) -> Iterator[ContextRegister]: """Yield contexts establishing that if self is always valid, other is sometimes valid.""" context = context or ContextRegister() if isinstance(other, self.__class__): yield from self._explain_implication_of_procedure_all_to_some( other=other, context=context)
def union( self, other: Union[Rule, Holding], context: Optional[ContextRegister] = None ) -> Optional[Holding]: """Infer a Holding from all inputs and outputs of self and other, in context.""" context = context or ContextRegister() if isinstance(other, Rule): other = Holding(rule=other) if not isinstance(other, Holding): raise TypeError return self._union_with_holding(other, context=context)
def comparison( self, operation: Callable, still_need_matches: Sequence[Factor], matches: ContextRegister = None, ) -> Iterator[ContextRegister]: r""" Find ways for two unordered sets of :class:`.Factor`\s to satisfy a comparison. All of the elements of `other` need to fit the comparison. The elements of `self` don't all need to be used. :param context: a mapping of :class:`.Factor`\s that have already been matched to each other in the recursive search for a complete group of matches. Usually starts empty when the method is first called. :param still_need_matches: :class:`.Factor`\s that need to satisfy the comparison :attr:`comparison` with some :class:`.Factor` of :attr:`available` for the relation to hold, and have not yet been matched. :param matches: a :class:`.ContextRegister` matching generic :class:`.Factor`\s :yields: context registers showing how each :class:`.Factor` in ``need_matches`` can have the relation ``comparison`` with some :class:`.Factor` in ``available_for_matching``, with matching context. """ still_need_matches = list(still_need_matches) if matches is None: matches = ContextRegister() if not still_need_matches: yield matches else: other_factor = still_need_matches.pop() for self_factor in self: if operation(self_factor, other_factor): updated_mappings = iter( self_factor.update_context_register( other=other_factor, register=matches, comparison=operation)) for new_matches in updated_mappings: if new_matches is not None: yield from iter( self.comparison( still_need_matches=still_need_matches, operation=operation, matches=new_matches, ))
def test_registers_for_interchangeable_context(self, make_entity, watt_factor): """ Test that _registers_for_interchangeable_context swaps the first two items in the ContextRegister """ matches = ContextRegister({ make_entity["motel"]: make_entity["trees"], make_entity["trees"]: make_entity["motel"], make_entity["watt"]: make_entity["watt"], }) new_matches = [ match for match in watt_factor["f7"]._registers_for_interchangeable_context(matches) ] assert (ContextRegister({ make_entity["trees"]: make_entity["trees"], make_entity["motel"]: make_entity["motel"], make_entity["watt"]: make_entity["watt"], }) in new_matches)
def explanations_same_meaning(self, other: Comparable, context: Optional[ContextRegister] = None ) -> Iterator[ContextRegister]: """Generate ways to match contexts of self and other so they mean the same.""" if (isinstance(other, Factor) and self.__class__ == other.__class__ and self.absent == other.absent and self.generic == other.generic): if self.generic: yield ContextRegister({self: other}) yield from self._means_if_concrete(other, context)
def _implies_if_present(self, other: Factor, context: Optional[ContextRegister] = None ) -> Iterator[ContextRegister]: """ Find if ``self`` would imply ``other`` if they aren't absent. :returns: bool indicating whether ``self`` would imply ``other``, under the assumption that neither self nor other has the attribute ``absent == True``. """ if context is None: context = ContextRegister() if isinstance(other, self.__class__): if other.generic: if context.get(self) is None or (context.get(self) == other): yield ContextRegister({self: other}) if not self.generic: yield from self._implies_if_concrete(other, context)
def test_likely_context_from_factor_meaning(self, make_opinion_with_holding): lotus = make_opinion_with_holding["lotus_majority"] oracle = make_opinion_with_holding["oracle_majority"] left = lotus.holdings[2].outputs[0] right = oracle.holdings[2].outputs[0] likely = left._likely_context_from_meaning(right, context=ContextRegister()) lotus_menu = lotus.holdings[2].generic_factors[0] java_api = oracle.generic_factors[0] assert likely[lotus_menu] == java_api
def explanations_shares_all_factors_with( self, other: ComparableGroup, context: Optional[ContextRegister] = None ) -> Iterator[ContextRegister]: context = context or ContextRegister() context_for_other = context.reversed() yield from (context.reversed() for context in other.comparison( operation=means, still_need_matches=list(self), matches=context_for_other, ))
def explanations_implication( self, other: ComparableGroup, context: Optional[ContextRegister] = None ) -> Iterator[Explanation]: explanation = Explanation(matches=[], context=context or ContextRegister()) yield from self.verbose_comparison( operation=operator.ge, still_need_matches=list(other), explanation=explanation, )
def _contradicts_if_not_exclusive( self, other: Holding, context: ContextRegister = None ) -> Iterator[ContextRegister]: if context is None: context = ContextRegister() if isinstance(other, Holding) and other.decided: if self.decided: yield from self._explanations_implies_if_not_exclusive( other.negated(), context=context ) else: yield from chain( other._implies_if_decided(self), other._implies_if_decided(self.negated()), )
def likely_contexts(self, other: Comparable, context: Optional[ContextRegister] = None ) -> Iterator[ContextRegister]: context = context or ContextRegister() same_meaning = self._likely_context_from_meaning(other, context) if same_meaning: implied = self._likely_context_from_implication( other, same_meaning) else: implied = self._likely_context_from_implication(other, context) if implied: yield implied if same_meaning: yield same_meaning yield context
def internally_consistent(self, context: Optional[ContextRegister] = None ) -> bool: """ Check for contradictions among the Factors in self. :returns: bool indicating whether self is internally consistent """ context = context or ContextRegister() unchecked = list(self) while unchecked: current = unchecked.pop() for item in unchecked: if not current.consistent_with(item, context): return False return True
def consistent_with( self, other: ComparableGroup, context: Optional[ContextRegister] = None, ) -> bool: r""" Find whether two sets of :class:`.Factor`\s can be consistent. Works by first determining whether one :class:`.Factor` potentially :meth:`~.Factor.contradicts` another, and then determining whether it's possible to make context assignments match between the contradictory :class:`.Factor`\s. .. Note:: Does ``Factor: None`` in matches always mean that the :class:`.Factor` can avoid being matched in a contradictory way? :param context: correspondences between :class:`Factor`s in self and other that can't be changed in seeking a way to interpret the groups as consistent :returns: whether unassigned context factors can be assigned in such a way that there's no contradiction between any factor in ``self_factors`` and ``other_factors``, given that some :class:`.Factor`\s have already been assigned as described by ``matches``. """ if context is None: context = ContextRegister() for self_factor in self: for other_factor in other: if self_factor.contradicts(other_factor): if all( all( context.get(key) == context_register[key] or context.get(context_register[key]) == key for key in self_factor.generic_factors) for context_register in self_factor. _context_registers(other_factor, means)): return False return True
def explanations_consistent_with_factor( self, other: Factor, context: Optional[ContextRegister] = None ) -> Iterator[ContextRegister]: """ Test whether ``self`` does not contradict ``other``. This should only be called after confirming that ``other`` is not ``None``. :returns: ``True`` if self and other can't both be true at the same time. Otherwise returns ``False``. """ if context is None: context = ContextRegister() for possible in self.possible_contexts(other, context): if not self.contradicts(other, context=possible): yield possible
def _context_registers( self, other: Optional[ComparableGroup], comparison: Callable, context: Optional[ContextRegister] = None, ) -> Iterator[ContextRegister]: r""" Search for ways to match :attr:`context_factors` of ``self`` and ``other``. :yields: all valid ways to make matches between corresponding :class:`Factor`\s. """ if context is None: context = ContextRegister() if other is None: yield context else: yield from self.comparison(operation=comparison, still_need_matches=list(other), matches=context)