Code cleanup

This commit is contained in:
Brandon Rozek 2025-05-03 16:42:15 -04:00
parent fa9e5026ca
commit 01204a9551
4 changed files with 286 additions and 353 deletions

200
vsp.py
View file

@ -2,6 +2,7 @@
Check to see if the model has the variable
sharing property.
"""
from collections import defaultdict
from itertools import chain, combinations, product
from typing import List, Optional, Set, Tuple
from common import set_to_str
@ -9,75 +10,36 @@ from model import (
Model, model_closure, ModelFunction, ModelValue, OrderTable
)
def preseed(
initial_set: Set[ModelValue],
cache:List[Tuple[Set[ModelValue], Set[ModelValue]]]):
"""
Given a cache of previous model_closure calls,
use this to compute an initial model closure
set based on the initial set.
class Cache:
def __init__(self):
# input size -> cached (inputs, outputs)
self.c = defaultdict(list)
Basic Idea:
Let {1, 2, 3} -> X be in the cache.
If {1,2,3} is a subset of initial set,
then X is the subset of the output of model_closure.
def add(self, i: Set[ModelValue], o: Set[ModelValue]):
self.c[len(i)].append((i, o))
This is used to speed up subsequent calls to model_closure
"""
candidate_preseed: Tuple[Set[ModelValue], int] = (None, None)
def get_closest(self, initial_set: Set[ModelValue]) -> Optional[Tuple[Set[ModelValue], bool]]:
"""
Iterate through our cache starting with the cached
inputs closest in size to the initial_set and
find the one that's a subset of initial_set.
for i, o in cache:
if i < initial_set:
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]:
candidate_preseed = o, cost
Returns cached_output, and whether the initial_set is the same
as the cached_input.
"""
initial_set_size = len(initial_set)
sizes = range(initial_set_size, 0, -1)
same_set = candidate_preseed[1] == 0
return candidate_preseed[0], same_set
for size in sizes:
if size not in self.c:
continue
for cached_input, cached_output in self.c[size]:
if cached_input <= initial_set:
return cached_output, size == initial_set_size
def find_top(algebra: Set[ModelValue], mconjunction: Optional[ModelFunction], mdisjunction: Optional[ModelFunction]) -> Optional[ModelValue]:
"""
Find the top of the order lattice.
T || a = T, T && a = a for all a in the carrier set
"""
if mconjunction is None or mdisjunction is None:
return None
for x in algebra:
is_top = True
for y in algebra:
if mdisjunction(x, y) != x or mconjunction(x, y) != y:
is_top = False
break
if is_top:
return x
print("[Warning] Failed to find the top of the lattice")
return None
def find_bottom(algebra: Set[ModelValue], mconjunction: Optional[ModelFunction], mdisjunction: Optional[ModelFunction]) -> Optional[ModelValue]:
"""
Find the bottom of the order lattice
F || a = a, F && a = F for all a in the carrier set
"""
if mconjunction is None or mdisjunction is None:
return None
for x in algebra:
is_bottom = True
for y in algebra:
if mdisjunction(x, y) != y or mconjunction(x, y) != x:
is_bottom = False
break
if is_bottom:
return x
print("[Warning] Failed to find the bottom of the lattice")
return None
def order_dependent(subalgebra1: Set[ModelValue], subalegbra2: Set[ModelValue], ordering: OrderTable):
"""
Returns true if there exists a value in subalgebra1 that's less than a value in subalgebra2
@ -106,17 +68,49 @@ Subalgebra 1: {set_to_str(self.subalgebra1)}
Subalgebra 2: {set_to_str(self.subalgebra2)}
"""
def quick_vsp_unsat_incomplete(xs, ys, model, top, bottom, negation_defined) -> bool:
"""
Return True if VSP cannot be satisfied
through some incomplete checks.
"""
# If the left subalgebra contains bottom
# or the right subalgebra contains top
# skip this pair
if top is not None and top in ys:
return True
if bottom is not None and bottom in xs:
return True
# If a subalgebra doesn't have at least one
# designated value, move onto the next pair.
# Depends on no intersection between xs and ys
if xs.isdisjoint(model.designated_values):
return True
if ys.isdisjoint(model.designated_values):
return True
# If the two subalgebras intersect, move
# onto the next pair.
if not xs.isdisjoint(ys):
return True
# If the subalgebras are order-dependent, skip this pair
if order_dependent(xs, ys, model.ordering):
return True
if negation_defined and order_dependent(ys, xs, model.ordering):
return True
# We can't immediately rule out that these
# subalgebras don't exhibit VSP
return False
def has_vsp(model: Model, impfunction: ModelFunction,
mconjunction: Optional[ModelFunction] = None,
mdisjunction: Optional[ModelFunction] = None,
mnegation: Optional[ModelFunction] = None) -> VSP_Result:
negation_defined: bool) -> VSP_Result:
"""
Checks whether a model has the variable
sharing property.
"""
top = find_top(model.carrier_set, mconjunction, mdisjunction)
bottom = find_bottom(model.carrier_set, mconjunction, mdisjunction)
# NOTE: No models with only one designated
# value satisfies VSP
if len(model.designated_values) == 1:
@ -124,68 +118,44 @@ def has_vsp(model: Model, impfunction: ModelFunction,
assert model.ordering is not None, "Expected ordering table in model"
top = model.ordering.top()
bottom = model.ordering.bottom()
# Compute I the set of tuples (x, y) where
# x -> y does not take a designiated value
I: Set[Tuple[ModelValue, ModelValue]] = set()
I: List[Tuple[ModelValue, ModelValue]] = []
for (x, y) in product(model.carrier_set, model.carrier_set):
if impfunction(x, y) not in model.designated_values:
I.add((x, y))
I.append((x, y))
# Construct the powerset of I without the empty set
s = list(I)
I_power = chain.from_iterable(combinations(s, r) for r in range(1, len(s) + 1))
I_power = chain.from_iterable(combinations(I, r) for r in range(1, len(I) + 1))
# ((x1, y1)), ((x1, y1), (x2, y2)), ...
# Closure cache
closure_cache: List[Tuple[Set[ModelValue], Set[ModelValue]]] = []
closure_cache = Cache()
# Find the subalgebras which falsify implication
for xys in I_power:
xs = {xy[0] for xy in xys}
xs = { xy[0] for xy in xys }
ys = { xy[1] for xy in xys }
if quick_vsp_unsat_incomplete(xs, ys, model, top, bottom, negation_defined):
continue
orig_xs = xs
cached_xs = preseed(xs, closure_cache)
if cached_xs[0] is not None:
cached_xs = closure_cache.get_closest(xs)
if cached_xs is not None:
xs |= cached_xs[0]
ys = {xy[1] for xy in xys}
orig_ys = ys
cached_ys = preseed(ys, closure_cache)
if cached_ys[0] is not None:
cached_ys = closure_cache.get_closest(ys)
if cached_ys is not None:
ys |= cached_ys[0]
# NOTE: Optimziation before model_closure
# If the two subalgebras intersect, move
# onto the next pair.
if len(xs & ys) > 0:
continue
# NOTE: Optimization
# If a subalgebra doesn't have at least one
# designated value, move onto the next pair.
# Depends on no intersection between xs and ys
if len(xs & model.designated_values) == 0:
continue
if len(ys & model.designated_values) == 0:
continue
# NOTE: Optimization
# If the left subalgebra contains bottom
# or the right subalgebra contains top
# skip this pair
if top is not None and top in ys:
continue
if bottom is not None and bottom in xs:
continue
# NOTE: Optimization
# If the subalgebras are order-dependent, skip this pair
if order_dependent(xs, ys, model.ordering):
continue
if mnegation is not None and order_dependent(ys, xs, model.ordering):
xs_ys_updated = cached_xs is not None or cached_ys is not None
if xs_ys_updated and quick_vsp_unsat_incomplete(xs, ys, model, top, bottom, negation_defined):
continue
# Compute the closure of all operations
@ -193,8 +163,8 @@ def has_vsp(model: Model, impfunction: ModelFunction,
carrier_set_left: Set[ModelValue] = model_closure(xs, model.logical_operations, bottom)
# Save to cache
if cached_xs[0] is not None and not cached_ys[1]:
closure_cache.append((orig_xs, carrier_set_left))
if cached_xs is None or (cached_xs is not None and not cached_xs[1]):
closure_cache.add(orig_xs, carrier_set_left)
if bottom is not None and bottom in carrier_set_left:
continue
@ -204,15 +174,15 @@ def has_vsp(model: Model, impfunction: ModelFunction,
carrier_set_right: Set[ModelValue] = model_closure(ys, model.logical_operations, top)
# Save to cache
if cached_ys[0] is not None and not cached_ys[1]:
closure_cache.append((orig_ys, carrier_set_right))
if cached_ys is None or (cached_ys is not None and not cached_ys[1]):
closure_cache.add(orig_ys, carrier_set_right)
if top is not None and top in carrier_set_right:
continue
# If the carrier set intersects, then move on to the next
# subalgebra
if len(carrier_set_left & carrier_set_right) > 0:
if not carrier_set_left.isdisjoint(carrier_set_right):
continue
# See if for all pairs in the subalgebras, that