-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathResolutionModel.py
More file actions
278 lines (233 loc) · 11 KB
/
ResolutionModel.py
File metadata and controls
278 lines (233 loc) · 11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
from Literal import Literal
from Clause import Clause
from functools import reduce
class ResolutionModel:
"""Represents a full CNF resolution model consisting of multiple clauses."""
def __init__(self, clauses: list):
"""
Initialize a ResolutionModel with a list of unique Clauses.
Args:
clauses: List of Clause objects (must not be empty)
Raises:
ValueError: If the list is empty
TypeError: If clauses is not a list
"""
if not isinstance(clauses, list):
raise TypeError(f"clauses must be a list, got: {type(clauses).__name__}")
if len(clauses) == 0:
raise ValueError("clauses list cannot be empty")
if not all(isinstance(c, Clause) for c in clauses):
raise TypeError("all items in clauses list must be Clause objects")
# Ensure uniqueness while preserving order
seen = set()
unique_clauses = []
for c in clauses:
if c not in seen:
unique_clauses.append(c)
seen.add(c)
self.__clauses = unique_clauses
def get_clauses(self) -> list:
"""Return a copy of the list of clauses in this resolution model"""
return self.__clauses.copy()
def __repr__(self) -> str:
"""String representation of the resolution model"""
return f"ResolutionModel({{{', '.join(str(c) for c in self.__clauses)}}})"
def __eq__(self, other) -> bool:
"""Check equality between two ResolutionModels"""
if not isinstance(other, ResolutionModel):
return False
return self.__clauses == other.__clauses
def __hash__(self) -> int:
"""Make ResolutionModel hashable for use in sets"""
return hash(tuple(self.__clauses))
def num_clauses(self) -> int:
"""Get the number of clauses in this model"""
return len(self.__clauses)
def resolve(self, index1: int, index2: int, literal: Literal) -> None:
"""
Resolve two clauses in the model on a given literal.
Args:
index1: Index of the first clause
index2: Index of the second clause
literal: The literal to resolve on
Raises:
IndexError: If either index is out of range
TypeError: If literal is not a Literal object
ValueError: If the clauses cannot be resolved on the given literal
"""
if not isinstance(literal, Literal):
raise TypeError(f"literal must be a Literal object, got: {type(literal).__name__}")
if index1 < 0 or index1 >= len(self.__clauses):
raise IndexError(f"index1 {index1} is out of range for clauses list of length {len(self.__clauses)}")
if index2 < 0 or index2 >= len(self.__clauses):
raise IndexError(f"index2 {index2} is out of range for clauses list of length {len(self.__clauses)}")
clause1 = self.__clauses[index1]
clause2 = self.__clauses[index2]
new_clause = Clause.resolve(clause1, clause2, literal)
if new_clause not in self.__clauses:
self.__clauses.append(new_clause)
def numResolveLiterals(self, index1: int, index2: int) -> int:
"""
Return the number of literal-negation pairs between the clauses at index1 and index2.
Raises IndexError for invalid indices.
"""
if index1 < 0 or index1 >= len(self.__clauses):
raise IndexError(f"index1 {index1} is out of range for clauses list of length {len(self.__clauses)}")
if index2 < 0 or index2 >= len(self.__clauses):
raise IndexError(f"index2 {index2} is out of range for clauses list of length {len(self.__clauses)}")
clause1 = self.__clauses[index1]
clause2 = self.__clauses[index2]
literals1 = clause1.get_literals()
literals2 = clause2.get_literals()
# Find all literal-negation pairs
pairs = 0
for lit1 in literals1:
for lit2 in literals2:
if lit1.letter == lit2.letter and lit1.is_negated != lit2.is_negated:
pairs += 1
return pairs
def getEasyLiteral(self, index1: int, index2: int) -> Literal:
"""
Returns a literal from clause at index1 that has its negation in clause at index2,
or vice versa. Raises IndexError for invalid indices. Returns the first such literal found.
Raises ValueError if no such literal exists.
"""
if index1 < 0 or index1 >= len(self.__clauses):
raise IndexError(f"index1 {index1} is out of range for clauses list of length {len(self.__clauses)}")
if index2 < 0 or index2 >= len(self.__clauses):
raise IndexError(f"index2 {index2} is out of range for clauses list of length {len(self.__clauses)}")
clause1 = self.__clauses[index1]
clause2 = self.__clauses[index2]
literals1 = clause1.get_literals()
literals2 = clause2.get_literals()
for lit1 in literals1:
for lit2 in literals2:
if lit1.letter == lit2.letter and lit1.is_negated != lit2.is_negated:
return lit1
for lit2 in literals2:
for lit1 in literals1:
if lit2.letter == lit1.letter and lit2.is_negated != lit1.is_negated:
return lit2
raise ValueError("No literal-negation pair found between the two clauses.")
def get_proof(self) -> str:
"""
Generate a proof of resolution steps leading to the empty clause, if it exists.
Returns:
A string representation of the proof steps, or a message indicating no proof exists.
Raises:
ValueError: If no empty clause exists in the model
"""
empty_clause = None
for clause in self.__clauses:
if len(clause.get_literals()) == 0:
empty_clause = clause
break
if empty_clause is None:
raise ValueError("No empty clause exists in the model; cannot generate proof.")
proof_list = []
seen = set()
def backtrack(clause: Clause):
if clause is None:
return
left, right = clause.get_parents()
backtrack(left)
backtrack(right)
if clause not in seen:
proof_list.append(clause)
seen.add(clause)
backtrack(empty_clause)
all_input_clause_at_front = []
for clause in proof_list:
if clause.get_parents() == (None, None):
all_input_clause_at_front.append(clause)
for clause in proof_list:
if clause.get_parents() != (None, None):
all_input_clause_at_front.append(clause)
proof_str = ""
for i in range(len(all_input_clause_at_front)):
end_str = "Input clause" if all_input_clause_at_front[i].get_parents() == (None, None) else (str(all_input_clause_at_front.index(all_input_clause_at_front[i].get_parents()[0]) + 1) + "," + str(all_input_clause_at_front.index(all_input_clause_at_front[i].get_parents()[1]) + 1) + " Resolution")
proof_str += f"{i+1:<5} {str(all_input_clause_at_front[i]):<20} {end_str:>20}\n"
return proof_str
def get_literal_negation_pairs(self, index1: int, index2: int) -> list:
"""
Returns a list of non-negated literals from clause at index1 or index2
that have their negation in the other clause. Raises IndexError for invalid indices.
"""
if index1 < 0 or index1 >= len(self.__clauses):
raise IndexError(f"index1 {index1} is out of range for clauses list of length {len(self.__clauses)}")
if index2 < 0 or index2 >= len(self.__clauses):
raise IndexError(f"index2 {index2} is out of range for clauses list of length {len(self.__clauses)}")
clause1 = self.__clauses[index1]
clause2 = self.__clauses[index2]
literals1 = clause1.get_literals()
literals2 = clause2.get_literals()
pairs = []
# Only add the non-negated literal for each pair
for lit1 in literals1:
for lit2 in literals2:
if lit1.letter == lit2.letter and lit1.is_negated != lit2.is_negated:
if not lit1.is_negated:
pairs.append(lit1)
elif not lit2.is_negated:
pairs.append(lit2)
# Remove duplicates (by letter)
unique_pairs = []
seen = set()
for lit in pairs:
key = lit.letter
if key not in seen:
unique_pairs.append(lit)
seen.add(key)
return unique_pairs
@staticmethod
def parse(s: str) -> 'ResolutionModel':
"""
Parse a string and return the corresponding ResolutionModel object.
Accepts formats like:
- "{C, D}, {A, ~B}"
- "{A, B} & {~B, C}"
- "(A B) ∧ (~B C)"
- "{{A, B}, {C, D}}"
- Each clause should be in the format accepted by Clause.parse()
Args:
s: String to parse
Returns:
A ResolutionModel object containing the parsed clauses
Raises:
ValueError: If the string is not in the correct format or contains invalid clauses
TypeError: If the input is not a string
"""
if not isinstance(s, str):
raise TypeError(f"parse() requires a string, got: {type(s).__name__}")
cleaned = s.strip()
if not cleaned:
raise ValueError("parse() requires a non-empty string")
import re
# Try to find all substrings that look like { ... }
clause_strings = re.findall(r'\{[^}]*\}', cleaned)
if not clause_strings:
# Try to find all substrings that look like ( ... )
clause_strings = re.findall(r'\([^)]*\)', cleaned)
if not clause_strings:
# Try to find all substrings that look like [ ... ]
clause_strings = re.findall(r'\[[^\]]*\]', cleaned)
if not clause_strings:
# If no {...}, [ ... ], or (...) found, fall back to previous splitting logic
cleaned = cleaned.replace('&', ' ').replace('∧', ' ')
clause_strings = cleaned.split()
if not clause_strings:
raise ValueError("parse() resulted in no valid clauses")
clauses = []
for clause_str in clause_strings:
clause_str = clause_str.strip()
if not clause_str:
continue
try:
clause = Clause.parse(clause_str)
if clause not in clauses:
clauses.append(clause)
except ValueError as e:
raise ValueError(f"Invalid clause in model: {e}")
if not clauses:
raise ValueError("parse() resulted in no valid clauses")
return ResolutionModel(clauses)