matmod/vsp.py

194 lines
6.5 KiB
Python

"""
Check to see if the model has the variable
sharing property.
"""
from itertools import chain, combinations, product
from typing import Dict, List, Optional, Set, Tuple
from common import set_to_str
from model import (
Model, model_closure, ModelFunction, ModelValue
)
from logic import Conjunction, Disjunction, Implication, Operation
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.
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.
This is used to speed up subsequent calls to model_closure
"""
candidate_preseed: Tuple[Set[ModelValue], int] = (None, None)
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
same_set = candidate_preseed[1] == 0
return candidate_preseed[0], same_set
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:
for y in algebra:
if mdisjunction(x, y) == x and mconjunction(x, y) == y:
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:
for y in algebra:
if mdisjunction(x, y) == y and mconjunction(x, y) == x:
return x
print("[Warning] Failed to find the bottom of the lattice")
return None
class VSP_Result:
def __init__(
self, has_vsp: bool, model_name: Optional[str] = None,
subalgebra1: Optional[Set[ModelValue]] = None,
subalgebra2: Optional[Set[ModelValue]] = None):
self.has_vsp = has_vsp
self.model_name = model_name
self.subalgebra1 = subalgebra1
self.subalgebra2 = subalgebra2
def __str__(self):
if not self.has_vsp:
return f"Model {self.model_name} does not have the variable sharing property."
return f"""Model {self.model_name} has the variable sharing property.
Subalgebra 1: {set_to_str(self.subalgebra1)}
Subalgebra 2: {set_to_str(self.subalgebra2)}
"""
def has_vsp(model: Model, interpretation: Dict[Operation, ModelFunction]) -> VSP_Result:
"""
Checks whether a model has the variable
sharing property.
"""
impfunction = interpretation[Implication]
mconjunction = interpretation.get(Conjunction)
mdisjunction = interpretation.get(Disjunction)
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:
return VSP_Result(False, model.name)
# Compute I the set of tuples (x, y) where
# x -> y does not take a designiated value
I: Set[Tuple[ModelValue, ModelValue]] = set()
for (x, y) in product(model.carrier_set, model.carrier_set):
if impfunction(x, y) not in model.designated_values:
I.add((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))
# ((x1, y1)), ((x1, y1), (x2, y2)), ...
# Closure cache
closure_cache: List[Tuple[Set[ModelValue], Set[ModelValue]]] = []
# Find the subalgebras which falsify implication
for xys in I_power:
xs = {xy[0] for xy in xys}
orig_xs = xs
cached_xs = preseed(xs, closure_cache)
if cached_xs[0] 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:
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 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
# Compute the closure of all operations
# with just the xs
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 bottom is not None and bottom in carrier_set_left:
continue
# Compute the closure of all operations
# with just the ys
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 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:
continue
# See if for all pairs in the subalgebras, that
# implication is falsified
falsified = True
for (x2, y2) in product(carrier_set_left, carrier_set_right):
if impfunction(x2, y2) in model.designated_values:
falsified = False
break
if falsified:
return VSP_Result(True, model.name, carrier_set_left, carrier_set_right)
return VSP_Result(False, model.name)