Code cleanup and documentation

This commit is contained in:
Brandon Rozek 2024-05-28 14:50:31 -04:00
parent 81a2d17965
commit 6b4d5828c8
No known key found for this signature in database
GPG key ID: 26E457DA82C9F480
3 changed files with 118 additions and 108 deletions

21
R.py
View file

@ -2,17 +2,17 @@
Modeling the logic R Modeling the logic R
""" """
from logic import ( from logic import (
Conjunction,
Disjunction,
Implication,
Logic,
Negation,
PropositionalVariable, PropositionalVariable,
Rule, Rule,
Logic,
Implication,
Conjunction,
Negation,
Disjunction,
Rule,
) )
from model import Model, ModelFunction, ModelValue, has_vsp, satisfiable from model import Model, ModelFunction, ModelValue, satisfiable
from generate_model import generate_model from generate_model import generate_model
from vsp import has_vsp
# =================================================== # ===================================================
@ -63,7 +63,7 @@ R_logic = Logic(operations, logic_rules)
# =============================== # ===============================
# Example Model of R # Example 2-element Model of R
a0 = ModelValue("a0") a0 = ModelValue("a0")
@ -123,13 +123,14 @@ solutions = generate_model(R_logic, model_size, print_model=True)
print(f"There are {len(solutions)} satisfiable models of element length {model_size}") print(f"There are {len(solutions)} satisfiable models of element length {model_size}")
for model, interpretation in solutions: for model, interpretation in solutions:
print("Has VSP?", has_vsp(model, interpretation)) print(has_vsp(model, interpretation))
print("-" * 5) print("-" * 5)
###### ######
# Smallest model for R that has the variable sharing property # Smallest model for R that has the variable sharing property
# This has 6 elements
a0 = ModelValue("a0") a0 = ModelValue("a0")
a1 = ModelValue("a1") a1 = ModelValue("a1")
@ -299,4 +300,4 @@ print(R_model_6)
print("Satisfiable", satisfiable(R_logic, R_model_6, interpretation)) print("Satisfiable", satisfiable(R_logic, R_model_6, interpretation))
print("Has VSP?", has_vsp(R_model_6, interpretation)) print(has_vsp(R_model_6, interpretation))

149
model.py
View file

@ -1,21 +1,20 @@
""" """
Defining what it means to be a model Matrix model semantics and satisfiability of
a given logic.
""" """
from common import set_to_str from common import set_to_str
from logic import ( from logic import (
PropositionalVariable, get_propostional_variables, Logic, Term, get_propostional_variables, Logic,
Operation, Conjunction, Disjunction, Implication Operation, PropositionalVariable, Term
) )
from typing import Set, Dict, Tuple, Optional from collections import defaultdict
from functools import lru_cache from functools import lru_cache
from itertools import combinations, chain, product, permutations from itertools import combinations_with_replacement, permutations, product
from copy import deepcopy from typing import Dict, List, Set, Tuple
__all__ = ['ModelValue', 'ModelFunction', 'Model'] __all__ = ['ModelValue', 'ModelFunction', 'Model']
class ModelValue: class ModelValue:
def __init__(self, name): def __init__(self, name):
self.name = name self.name = name
@ -29,10 +28,7 @@ class ModelValue:
return self.hashed_value return self.hashed_value
def __eq__(self, other): def __eq__(self, other):
return isinstance(other, ModelValue) and self.name == other.name return isinstance(other, ModelValue) and self.name == other.name
def __lt__(self, other): def __deepcopy__(self, _):
assert isinstance(other, ModelValue)
return ModelOrderConstraint(self, other)
def __deepcopy__(self, memo):
return ModelValue(self.name) return ModelValue(self.name)
@ -41,8 +37,9 @@ class ModelFunction:
self.operation_name = operation_name self.operation_name = operation_name
self.arity = arity self.arity = arity
# Correct input to always be a tuple # Transform the mapping such that the
corrected_mapping = dict() # key is always a tuple of model values
corrected_mapping: Dict[Tuple[ModelValue], ModelValue] = {}
for k, v in mapping.items(): for k, v in mapping.items():
if isinstance(k, tuple): if isinstance(k, tuple):
assert len(k) == arity assert len(k) == arity
@ -66,35 +63,17 @@ class ModelFunction:
def __call__(self, *args): def __call__(self, *args):
return self.mapping[args] return self.mapping[args]
# def __eq__(self, other):
# return isinstance(other, ModelFunction) and self.name == other.name and self.arity == other.arity
class ModelOrderConstraint:
# a < b
def __init__(self, a: ModelValue, b: ModelValue):
self.a = a
self.b = b
def __hash__(self):
return hash(self.a) * hash(self.b)
def __eq__(self, other):
return isinstance(other, ModelOrderConstraint) and \
self.a == other.a and self.b == other.b
class Model: class Model:
def __init__( def __init__(
self, self,
carrier_set: Set[ModelValue], carrier_set: Set[ModelValue],
logical_operations: Set[ModelFunction], logical_operations: Set[ModelFunction],
designated_values: Set[ModelValue], designated_values: Set[ModelValue],
ordering: Optional[Set[ModelOrderConstraint]] = None
): ):
assert designated_values <= carrier_set assert designated_values <= carrier_set
self.carrier_set = carrier_set self.carrier_set = carrier_set
self.logical_operations = logical_operations self.logical_operations = logical_operations
self.designated_values = designated_values self.designated_values = designated_values
self.ordering = ordering if ordering is not None else set()
# TODO: Make sure ordering is "valid"
# That is: transitive, etc.
def __str__(self): def __str__(self):
result = f"""Carrier Set: {set_to_str(self.carrier_set)} result = f"""Carrier Set: {set_to_str(self.carrier_set)}
@ -106,12 +85,22 @@ Designated Values: {set_to_str(self.designated_values)}
return result return result
def evaluate_term(t: Term, f: Dict[PropositionalVariable, ModelValue], interpretation: Dict[Operation, ModelFunction]) -> ModelValue: def evaluate_term(
t: Term, f: Dict[PropositionalVariable, ModelValue],
interpretation: Dict[Operation, ModelFunction]) -> ModelValue:
"""
Given a term in a logic, mapping
between terms and model values,
as well as an interpretation
of operations to model functions,
return the evaluated model value.
"""
if isinstance(t, PropositionalVariable): if isinstance(t, PropositionalVariable):
return f[t] return f[t]
model_function = interpretation[t.operation] model_function = interpretation[t.operation]
model_arguments = [] model_arguments: List[ModelValue] = []
for logic_arg in t.arguments: for logic_arg in t.arguments:
model_arg = evaluate_term(logic_arg, f, interpretation) model_arg = evaluate_term(logic_arg, f, interpretation)
model_arguments.append(model_arg) model_arguments.append(model_arg)
@ -121,11 +110,15 @@ def evaluate_term(t: Term, f: Dict[PropositionalVariable, ModelValue], interpret
def all_model_valuations( def all_model_valuations(
pvars: Tuple[PropositionalVariable], pvars: Tuple[PropositionalVariable],
mvalues: Tuple[ModelValue]): mvalues: Tuple[ModelValue]):
"""
Given propositional variables and model values,
produce every possible mapping between the two.
"""
all_possible_values = product(mvalues, repeat=len(pvars)) all_possible_values = product(mvalues, repeat=len(pvars))
for valuation in all_possible_values: for valuation in all_possible_values:
mapping: Dict[PropositionalVariable, ModelValue] = dict() mapping: Dict[PropositionalVariable, ModelValue] = {}
assert len(pvars) == len(valuation) assert len(pvars) == len(valuation)
for pvar, value in zip(pvars, valuation): for pvar, value in zip(pvars, valuation):
mapping[pvar] = value mapping[pvar] = value
@ -137,98 +130,92 @@ def all_model_valuations_cached(
mvalues: Tuple[ModelValue]): mvalues: Tuple[ModelValue]):
return list(all_model_valuations(pvars, mvalues)) return list(all_model_valuations(pvars, mvalues))
def rule_ordering_satisfied(model: Model, interpretation: Dict[Operation, ModelFunction]) -> bool:
"""
Currently testing whether this function helps with runtime...
"""
if Conjunction in interpretation:
possible_inputs = ((a, b) for (a, b) in product(model.carrier_set, model.carrier_set))
for a, b in possible_inputs:
output = interpretation[Conjunction](a, b)
if a < b in model.ordering and output != a:
print("RETURNING FALSE")
return False
if b < a in model.ordering and output != b:
print("RETURNING FALSE")
return False
if Disjunction in interpretation:
possible_inputs = ((a, b) for (a, b) in product(model.carrier_set, model.carrier_set))
for a, b in possible_inputs:
output = interpretation[Disjunction](a, b)
if a < b in model.ordering and output != b:
print("RETURNING FALSE")
return False
if b < a in model.ordering and output != a:
print("RETURNING FALSE")
return False
return True
def satisfiable(logic: Logic, model: Model, interpretation: Dict[Operation, ModelFunction]) -> bool: def satisfiable(logic: Logic, model: Model, interpretation: Dict[Operation, ModelFunction]) -> bool:
"""
Determine whether a model satisfies a logic
given an interpretation.
"""
pvars = tuple(get_propostional_variables(tuple(logic.rules))) pvars = tuple(get_propostional_variables(tuple(logic.rules)))
mappings = all_model_valuations_cached(pvars, tuple(model.carrier_set)) mappings = all_model_valuations_cached(pvars, tuple(model.carrier_set))
# NOTE: Does not look like rule ordering is helping for finding
# models of R...
if not rule_ordering_satisfied(model, interpretation):
return False
for mapping in mappings: for mapping in mappings:
# Make sure that the model satisfies each of the rules
for rule in logic.rules: for rule in logic.rules:
# The check only applies if the premises are designated
premise_met = True premise_met = True
premise_ts = set() premise_ts: Set[ModelValue] = set()
for premise in rule.premises: for premise in rule.premises:
premise_t = evaluate_term(premise, mapping, interpretation) premise_t = evaluate_term(premise, mapping, interpretation)
# As soon as one premise is not designated,
# move to the next rule.
if premise_t not in model.designated_values: if premise_t not in model.designated_values:
premise_met = False premise_met = False
break break
# If designated, keep track of the evaluated term
premise_ts.add(premise_t) premise_ts.add(premise_t)
if not premise_met: if not premise_met:
continue continue
# With the premises designated, make sure the consequent is designated
consequent_t = evaluate_term(rule.conclusion, mapping, interpretation) consequent_t = evaluate_term(rule.conclusion, mapping, interpretation)
if consequent_t not in model.designated_values: if consequent_t not in model.designated_values:
return False return False
return True return True
from itertools import combinations_with_replacement
from collections import defaultdict
def model_closure(initial_set: Set[ModelValue], mfunctions: Set[ModelFunction]): def model_closure(initial_set: Set[ModelValue], mfunctions: Set[ModelFunction]):
"""
Given an initial set of model values and a set of model functions,
compute the complete set of model values that are closed
under the operations.
"""
closure_set: Set[ModelValue] = initial_set closure_set: Set[ModelValue] = initial_set
last_new = initial_set last_new: Set[ModelValue] = initial_set
changed = True changed: bool = True
while changed: while changed:
changed = False changed = False
new_elements = set() new_elements: Set[ModelValue] = set()
old_closure = closure_set - last_new old_closure: Set[ModelValue] = closure_set - last_new
# arity -> args # arity -> args
cached_args = defaultdict(list) cached_args = defaultdict(list)
# Pass elements into each model function
for mfun in mfunctions: for mfun in mfunctions:
# Use cached args if this arity was looked at before # If a previous function shared the same arity,
# we'll use the same set of computed arguments
# to pass into the model functions.
if mfun.arity in cached_args: if mfun.arity in cached_args:
for args in cached_args[mfun.arity]: for args in cached_args[mfun.arity]:
# Compute the new elements
# given the cached arguments.
element = mfun(*args) element = mfun(*args)
if element not in closure_set: if element not in closure_set:
new_elements.add(element) new_elements.add(element)
# Move onto next function
# We don't need to compute the arguments
# thanks to the cache, so move onto the
# next function.
continue continue
# Iterate over how many new elements would be within the arguments # At this point, we don't have cached arguments, so we need
# NOTE: To not repeat work, there must be at least one new element # to compute this set.
# Each argument must have at least one new element to not repeat
# work. We'll range over the number of new model values within our
# argument.
for num_new in range(1, mfun.arity + 1): for num_new in range(1, mfun.arity + 1):
new_args = combinations_with_replacement(last_new, r=num_new) new_args = combinations_with_replacement(last_new, r=num_new)
old_args = combinations_with_replacement(old_closure, r=mfun.arity - num_new) old_args = combinations_with_replacement(old_closure, r=mfun.arity - num_new)
# Determine every possible ordering of the concatenated
# new and old model values.
for new_arg, old_arg in product(new_args, old_args): for new_arg, old_arg in product(new_args, old_args):
for args in permutations(new_arg + old_arg): for args in permutations(new_arg + old_arg):
cached_args[mfun.arity].append(args) cached_args[mfun.arity].append(args)

52
vsp.py
View file

@ -3,40 +3,62 @@ Check to see if the model has the variable
sharing property. sharing property.
""" """
from itertools import chain, combinations, product from itertools import chain, combinations, product
from typing import Dict, Set, Tuple, List from typing import Dict, List, Optional, Set, Tuple
from model import ( from model import (
Model, ModelFunction, ModelValue, model_closure Model, model_closure, ModelFunction, ModelValue
) )
from logic import Implication, Operation from logic import Implication, Operation
def preseed(initial_set: Set[ModelValue], cache:List[Tuple[Set[ModelValue], Set[ModelValue]]]): def preseed(
initial_set: Set[ModelValue],
cache:List[Tuple[Set[ModelValue], Set[ModelValue]]]):
""" """
Cache contains caches of model closure calls: Given a cache of previous model_closure calls,
Ex: use this to compute an initial model closure
{1, 2, 3} -> {....} set based on the initial set.
Basic Idea:
Let {1, 2, 3} -> X be in the cache.
If {1,2,3} is a subset of initial set, If {1,2,3} is a subset of initial set,
then {....} is the subset of the output of model_closure. then X is the subset of the output of model_closure.
We'll use the output to speed up the saturation procedure This is used to speed up subsequent calls to model_closure
""" """
candidate_preseed: Tuple[Set[ModelValue], int] = (None, None) candidate_preseed: Tuple[Set[ModelValue], int] = (None, None)
for i, o in cache: for i, o in cache:
if i < initial_set: if i < initial_set:
cost = len(initial_set - i) cost = len(initial_set - i)
# If i is a subset with less missing elements than
# the previous candidate, then it's the new candidate.
if candidate_preseed[1] is None or cost < candidate_preseed[1]: if candidate_preseed[1] is None or cost < candidate_preseed[1]:
candidate_preseed = o, cost candidate_preseed = o, cost
same_set = candidate_preseed[1] == 0 same_set = candidate_preseed[1] == 0
return candidate_preseed[0], same_set return candidate_preseed[0], same_set
def has_vsp(model: Model, interpretation: Dict[Operation, ModelFunction]) -> bool: class VSP_Result:
""" def __init__(
Tells you whether a model has the self, has_vsp: bool, subalgebra1: Optional[Set[ModelValue]] = None,
variable sharing property. subalgebra2: Optional[Set[ModelValue]] = None, x: Optional[ModelValue] = None,
""" y: Optional[ModelValue] = None):
self.has_vsp = has_vsp
self.subalgebra1 = subalgebra1
self.subalgebra2 = subalgebra2
self.x = x
self.y = y
def __str__(self):
if self.has_vsp:
return "Model has the variable sharing property."
else:
return "Model does not have the variable sharing property."
def has_vsp(model: Model, interpretation: Dict[Operation, ModelFunction]) -> VSP_Result:
"""
Checks whether a model has the variable
sharing property.
"""
impfunction = interpretation[Implication] impfunction = interpretation[Implication]
# Compute I the set of tuples (x, y) where # Compute I the set of tuples (x, y) where
@ -109,6 +131,6 @@ def has_vsp(model: Model, interpretation: Dict[Operation, ModelFunction]) -> boo
break break
if falsified: if falsified:
return True return VSP_Result(True, carrier_set_left, carrier_set_right, x2, y2)
return False return VSP_Result(False)