Skip to content

Commit b62805a

Browse files
Merge branch 'main' into file-fix
2 parents 6b0112c + 92332ce commit b62805a

10 files changed

Lines changed: 629 additions & 548 deletions

File tree

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,4 +132,7 @@ dmypy.json
132132
.vscode
133133

134134
# Test reports
135-
reports/
135+
reports/
136+
137+
# lf_toolkit install
138+
src/

evaluation_function/correction/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,8 @@
99

1010
from .correction import (
1111
analyze_fsa_correction,
12-
check_minimality,
1312
)
1413

1514
__all__ = [
1615
"analyze_fsa_correction",
17-
"check_minimality",
1816
]

evaluation_function/correction/correction.py

Lines changed: 183 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -7,78 +7,72 @@
77
All detailed "why" feedback comes from are_isomorphic() in validation module.
88
"""
99

10-
from typing import List, Optional, Tuple
10+
from typing import List, Optional
11+
12+
from evaluation_function.schemas.params import Params
1113

1214
# Schema imports
13-
from ..schemas import FSA, ValidationError, ErrorCode
15+
from ..schemas import FSA, ValidationError, ErrorCode, ValidationResult
1416
from ..schemas.result import Result, FSAFeedback, StructuralInfo, LanguageComparison
1517

1618
# Validation imports
1719
from ..validation.validation import (
20+
are_isomorphic,
1821
is_valid_fsa,
22+
is_deterministic,
23+
is_complete,
24+
is_minimal,
1925
fsas_accept_same_language,
2026
get_structured_info_of_fsa,
2127
)
2228

23-
# Algorithm imports for minimality check
24-
from ..algorithms.minimization import hopcroft_minimization
25-
26-
27-
# =============================================================================
28-
# Minimality Check
29-
# =============================================================================
30-
31-
def _check_minimality(fsa: FSA) -> Tuple[bool, Optional[ValidationError]]:
32-
"""Check if FSA is minimal by comparing with its minimized version."""
33-
try:
34-
minimized = hopcroft_minimization(fsa)
35-
if len(minimized.states) < len(fsa.states):
36-
diff = len(fsa.states) - len(minimized.states)
37-
return False, ValidationError(
38-
message=f"Your FSA works correctly, but it's not minimal! You have {len(fsa.states)} states, but only {len(minimized.states)} are needed. You could remove {diff} state(s).",
39-
code=ErrorCode.NOT_MINIMAL,
40-
severity="error",
41-
suggestion="Look for states that behave identically (same transitions and acceptance) - these can be merged into one"
42-
)
43-
return True, None
44-
except Exception:
45-
return True, None
46-
47-
48-
def check_minimality(fsa: FSA) -> bool:
49-
"""Check if FSA is minimal."""
50-
is_min, _ = _check_minimality(fsa)
51-
return is_min
52-
5329

5430
# =============================================================================
55-
# Helper Functions
31+
# Feedback Helpers
5632
# =============================================================================
5733

5834
def _build_feedback(
5935
summary: str,
6036
validation_errors: List[ValidationError],
6137
equivalence_errors: List[ValidationError],
62-
structural_info: Optional[StructuralInfo]
38+
structural_info: Optional[StructuralInfo],
39+
params: Params
6340
) -> FSAFeedback:
6441
"""Build FSAFeedback from errors and analysis."""
6542
all_errors = validation_errors + equivalence_errors
43+
6644
errors = [e for e in all_errors if e.severity == "error"]
6745
warnings = [e for e in all_errors if e.severity in ("warning", "info")]
68-
69-
# Build hints from all error suggestions
70-
hints = [e.suggestion for e in all_errors if e.suggestion]
71-
if structural_info:
72-
if structural_info.unreachable_states:
73-
unreachable = ", ".join(structural_info.unreachable_states)
74-
hints.append(f"Tip: States {{{unreachable}}} can't be reached from your start state - you might want to remove them or add transitions to them")
75-
if structural_info.dead_states:
76-
dead = ", ".join(structural_info.dead_states)
77-
hints.append(f"Tip: States {{{dead}}} can never lead to acceptance - this might be intentional (trap states) or a bug")
78-
79-
# Build language comparison
80-
language = LanguageComparison(are_equivalent=len(equivalence_errors) == 0)
81-
46+
47+
# Remove UI highlights if disabled
48+
if not params.highlight_errors:
49+
for e in all_errors:
50+
e.highlight = None
51+
52+
# Build hints
53+
hints: List[str] = []
54+
if params.feedback_verbosity != "minimal":
55+
hints.extend(e.suggestion for e in all_errors if e.suggestion)
56+
57+
if params.feedback_verbosity == "detailed" and structural_info:
58+
if structural_info.unreachable_states:
59+
unreachable = ", ".join(structural_info.unreachable_states)
60+
hints.append(
61+
f"Tip: States {{{unreachable}}} are unreachable from the start state"
62+
)
63+
if structural_info.dead_states:
64+
dead = ", ".join(structural_info.dead_states)
65+
hints.append(
66+
f"Tip: States {{{dead}}} can never reach an accepting state"
67+
)
68+
else:
69+
structural_info = None
70+
hints = []
71+
72+
language = LanguageComparison(
73+
are_equivalent=len(equivalence_errors) == 0
74+
)
75+
8276
return FSAFeedback(
8377
summary=summary,
8478
errors=errors,
@@ -90,25 +84,25 @@ def _build_feedback(
9084

9185

9286
def _summarize_errors(errors: List[ValidationError]) -> str:
93-
"""Generate summary from error messages."""
94-
error_types = set()
87+
"""Generate a human-readable summary from error messages."""
88+
categories = set()
89+
9590
for error in errors:
9691
msg = error.message.lower()
9792
if "alphabet" in msg:
98-
error_types.add("alphabet issue")
99-
elif "states" in msg and ("many" in msg or "few" in msg or "needed" in msg):
100-
error_types.add("incorrect number of states")
101-
elif "accepting" in msg or "accept" in msg:
102-
error_types.add("accepting states issue")
103-
elif "transition" in msg or "reading" in msg:
104-
error_types.add("transition issue")
105-
106-
if len(error_types) == 1:
107-
issue = list(error_types)[0]
108-
return f"Almost there! Your FSA has an {issue}. Check the details below."
109-
elif error_types:
110-
return f"Your FSA doesn't quite match the expected language. Issues found: {', '.join(error_types)}"
111-
return f"Your FSA doesn't accept the correct language. Found {len(errors)} issue(s) to fix."
93+
categories.add("alphabet issue")
94+
elif "accept" in msg:
95+
categories.add("accepting states issue")
96+
elif "transition" in msg:
97+
categories.add("transition issue")
98+
elif "state" in msg:
99+
categories.add("state structure issue")
100+
101+
if len(categories) == 1:
102+
return f"Almost there! Your FSA has a {next(iter(categories))}."
103+
elif categories:
104+
return f"Your FSA has multiple issues: {', '.join(categories)}."
105+
return "Your FSA does not match the expected language."
112106

113107

114108
# =============================================================================
@@ -118,75 +112,148 @@ def _summarize_errors(errors: List[ValidationError]) -> str:
118112
def analyze_fsa_correction(
119113
student_fsa: FSA,
120114
expected_fsa: FSA,
121-
require_minimal: bool = False
115+
params: Params
122116
) -> Result:
123117
"""
124-
Compare student FSA against expected FSA.
125-
126-
Returns Result with:
127-
- is_correct: True if FSAs accept same language
128-
- feedback: Human-readable summary
129-
- fsa_feedback: Structured feedback with ElementHighlight for UI
130-
131-
Args:
132-
student_fsa: The student's FSA
133-
expected_fsa: The reference/expected FSA
134-
require_minimal: Whether to require student FSA to be minimal
118+
Compare student FSA against expected FSA using configurable parameters.
135119
"""
120+
136121
validation_errors: List[ValidationError] = []
137122
equivalence_errors: List[ValidationError] = []
138123
structural_info: Optional[StructuralInfo] = None
139-
124+
125+
# -------------------------------------------------------------------------
140126
# Step 1: Validate student FSA structure
141-
student_errors = is_valid_fsa(student_fsa)
142-
if student_errors:
143-
num_errors = len(student_errors)
144-
if num_errors == 1:
145-
summary = "Your FSA has a structural problem that needs to be fixed first. See the details below."
146-
else:
147-
summary = f"Your FSA has {num_errors} structural problems that need to be fixed first. See the details below."
127+
# -------------------------------------------------------------------------
128+
student_result = is_valid_fsa(student_fsa)
129+
if not student_result.ok:
130+
summary = (
131+
"Your FSA has a structural problem that needs to be fixed first."
132+
if len(student_result.errors) == 1
133+
else f"Your FSA has {len(student_result.errors)} structural problems to fix."
134+
)
148135
return Result(
149136
is_correct=False,
150137
feedback=summary,
151-
fsa_feedback=_build_feedback(summary, student_errors, [], None)
138+
fsa_feedback=_build_feedback(
139+
summary,
140+
student_result.errors,
141+
[],
142+
None,
143+
params
144+
)
152145
)
153-
154-
# Step 2: Validate expected FSA (should not fail)
155-
expected_errors = is_valid_fsa(expected_fsa)
156-
if expected_errors:
146+
147+
# -------------------------------------------------------------------------
148+
# Step 2: Validate expected FSA (should never fail)
149+
# -------------------------------------------------------------------------
150+
expected_result = is_valid_fsa(expected_fsa)
151+
if not expected_result.ok:
157152
return Result(
158153
is_correct=False,
159154
feedback="Oops! There's an issue with the expected answer. Please contact your instructor."
160155
)
161-
162-
# Step 3: Check minimality if required
163-
if require_minimal:
164-
is_min, min_error = _check_minimality(student_fsa)
165-
if not is_min and min_error:
166-
validation_errors.append(min_error)
167-
168-
# Step 4: Structural analysis
156+
157+
# -------------------------------------------------------------------------
158+
# Step 3: Enforce expected automaton type
159+
# -------------------------------------------------------------------------
160+
if params.expected_type == "DFA":
161+
det_result = is_deterministic(student_fsa)
162+
if not det_result.ok:
163+
summary = "Your automaton must be deterministic (a DFA)."
164+
return Result(
165+
is_correct=False,
166+
feedback=summary,
167+
fsa_feedback=_build_feedback(
168+
summary,
169+
det_result.errors,
170+
[],
171+
None,
172+
params
173+
)
174+
)
175+
176+
# -------------------------------------------------------------------------
177+
# Step 4: Optional completeness check
178+
# -------------------------------------------------------------------------
179+
if params.check_completeness:
180+
comp_result = is_complete(student_fsa)
181+
if not comp_result.ok:
182+
validation_errors.extend(comp_result.errors)
183+
184+
# -------------------------------------------------------------------------
185+
# Step 5: Optional minimality check
186+
# -------------------------------------------------------------------------
187+
validation_result = None
188+
if params.check_minimality:
189+
validation_result = is_minimal(student_fsa)
190+
if not validation_result.ok:
191+
validation_errors.extend(validation_result.errors)
192+
193+
# -------------------------------------------------------------------------
194+
# Step 6: Structural analysis (for feedback only)
195+
# -------------------------------------------------------------------------
169196
structural_info = get_structured_info_of_fsa(student_fsa)
170-
171-
# Step 5: Language equivalence (with detailed feedback from are_isomorphic)
172-
equivalence_errors = fsas_accept_same_language(student_fsa, expected_fsa)
173-
174-
if not equivalence_errors and not validation_errors:
175-
# Success message with some stats
176-
state_count = len(student_fsa.states)
177-
feedback = f"Correct! Your FSA with {state_count} state(s) accepts exactly the right language. Well done!"
178-
return Result(
179-
is_correct=True,
180-
feedback=feedback,
181-
fsa_feedback=_build_feedback("Your FSA is correct!", [], [], structural_info)
197+
198+
# -------------------------------------------------------------------------
199+
# Step 7: Language equivalence
200+
# -------------------------------------------------------------------------
201+
equivalence_result = fsas_accept_same_language(
202+
student_fsa, expected_fsa
203+
)
204+
equivalence_errors.extend(equivalence_result.errors)
205+
206+
# -------------------------------------------------------------------------
207+
# Step 8: Isomorphism
208+
# -------------------------------------------------------------------------
209+
iso_result = are_isomorphic(student_fsa, expected_fsa)
210+
equivalence_errors.extend(iso_result.errors)
211+
212+
# -------------------------------------------------------------------------
213+
# Step 9: Decide correctness based on evaluation mode
214+
# -------------------------------------------------------------------------
215+
if params.evaluation_mode == "strict":
216+
is_correct = (
217+
(not params.check_minimality or validation_result.ok)
218+
and equivalence_result.ok
219+
and iso_result.ok
220+
)
221+
elif params.evaluation_mode == "lenient":
222+
is_correct = (
223+
(not params.check_minimality or validation_result.ok)
224+
and equivalence_result.ok
182225
)
183-
184-
# Build result with errors
185-
is_correct = len(equivalence_errors) == 0 and len(validation_errors) == 0
186-
summary = _summarize_errors(equivalence_errors) if equivalence_errors else "Your FSA has some issues to address."
187-
226+
else: # partial # I dont know what the partial is meant for, always mark as incorrect?
227+
is_correct = False
228+
229+
# -------------------------------------------------------------------------
230+
# Step 10: Build summary
231+
# -------------------------------------------------------------------------
232+
if is_correct:
233+
feedback = (
234+
f"Correct! Your FSA with {len(student_fsa.states)} state(s) "
235+
"accepts exactly the right language. Well done!"
236+
)
237+
summary = "Your FSA is correct!"
238+
else:
239+
summary = (
240+
_summarize_errors(equivalence_errors)
241+
if len(equivalence_errors) > 0
242+
else "Your FSA has some issues to address."
243+
)
244+
feedback = summary
245+
246+
# -------------------------------------------------------------------------
247+
# Step 11: Return result
248+
# -------------------------------------------------------------------------
188249
return Result(
189250
is_correct=is_correct,
190-
feedback=summary,
191-
fsa_feedback=_build_feedback(summary, validation_errors, equivalence_errors, structural_info)
251+
feedback=feedback,
252+
fsa_feedback=_build_feedback(
253+
summary,
254+
validation_errors,
255+
equivalence_errors,
256+
structural_info,
257+
params
258+
)
192259
)

0 commit comments

Comments
 (0)