Skip to content

Commit d36b541

Browse files
authored
Merge pull request #43 from lambda-feedback/payload
fix: add config
2 parents 548a504 + b45fb8c commit d36b541

9 files changed

Lines changed: 519 additions & 607 deletions

File tree

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: 166 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
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
1315
from ..schemas import FSA, ValidationError, ErrorCode
@@ -16,69 +18,60 @@
1618
# Validation imports
1719
from ..validation.validation import (
1820
is_valid_fsa,
21+
is_deterministic,
22+
is_complete,
23+
is_minimal,
1924
fsas_accept_same_language,
2025
get_structured_info_of_fsa,
2126
)
2227

23-
# Algorithm imports for minimality check
24-
from ..algorithms.minimization import hopcroft_minimization
25-
2628

2729
# =============================================================================
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-
53-
54-
# =============================================================================
55-
# Helper Functions
30+
# Feedback Helpers
5631
# =============================================================================
5732

5833
def _build_feedback(
5934
summary: str,
6035
validation_errors: List[ValidationError],
6136
equivalence_errors: List[ValidationError],
62-
structural_info: Optional[StructuralInfo]
37+
structural_info: Optional[StructuralInfo],
38+
params: Params
6339
) -> FSAFeedback:
6440
"""Build FSAFeedback from errors and analysis."""
6541
all_errors = validation_errors + equivalence_errors
42+
6643
errors = [e for e in all_errors if e.severity == "error"]
6744
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-
45+
46+
# Remove UI highlights if disabled
47+
if not params.highlight_errors:
48+
for e in all_errors:
49+
e.highlight = None
50+
51+
# Build hints
52+
hints: List[str] = []
53+
if params.feedback_verbosity != "minimal":
54+
hints.extend(e.suggestion for e in all_errors if e.suggestion)
55+
56+
if params.feedback_verbosity == "detailed" and structural_info:
57+
if structural_info.unreachable_states:
58+
unreachable = ", ".join(structural_info.unreachable_states)
59+
hints.append(
60+
f"Tip: States {{{unreachable}}} are unreachable from the start state"
61+
)
62+
if structural_info.dead_states:
63+
dead = ", ".join(structural_info.dead_states)
64+
hints.append(
65+
f"Tip: States {{{dead}}} can never reach an accepting state"
66+
)
67+
else:
68+
structural_info = None
69+
hints = []
70+
71+
language = LanguageComparison(
72+
are_equivalent=len(equivalence_errors) == 0
73+
)
74+
8275
return FSAFeedback(
8376
summary=summary,
8477
errors=errors,
@@ -90,25 +83,25 @@ def _build_feedback(
9083

9184

9285
def _summarize_errors(errors: List[ValidationError]) -> str:
93-
"""Generate summary from error messages."""
94-
error_types = set()
86+
"""Generate a human-readable summary from error messages."""
87+
categories = set()
88+
9589
for error in errors:
9690
msg = error.message.lower()
9791
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."
92+
categories.add("alphabet issue")
93+
elif "accept" in msg:
94+
categories.add("accepting states issue")
95+
elif "transition" in msg:
96+
categories.add("transition issue")
97+
elif "state" in msg:
98+
categories.add("state structure issue")
99+
100+
if len(categories) == 1:
101+
return f"Almost there! Your FSA has a {next(iter(categories))}."
102+
elif categories:
103+
return f"Your FSA has multiple issues: {', '.join(categories)}."
104+
return "Your FSA does not match the expected language."
112105

113106

114107
# =============================================================================
@@ -118,75 +111,133 @@ def _summarize_errors(errors: List[ValidationError]) -> str:
118111
def analyze_fsa_correction(
119112
student_fsa: FSA,
120113
expected_fsa: FSA,
121-
require_minimal: bool = False
114+
params: Params
122115
) -> Result:
123116
"""
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
117+
Compare student FSA against expected FSA using configurable parameters.
135118
"""
119+
136120
validation_errors: List[ValidationError] = []
137121
equivalence_errors: List[ValidationError] = []
138122
structural_info: Optional[StructuralInfo] = None
139-
123+
124+
# -------------------------------------------------------------------------
140125
# 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."
126+
# -------------------------------------------------------------------------
127+
student_result = is_valid_fsa(student_fsa)
128+
if not student_result.ok():
129+
summary = (
130+
"Your FSA has a structural problem that needs to be fixed first."
131+
if len(student_result.errors) == 1
132+
else f"Your FSA has {len(student_result.errors)} structural problems to fix."
133+
)
148134
return Result(
149135
is_correct=False,
150136
feedback=summary,
151-
fsa_feedback=_build_feedback(summary, student_errors, [], None)
137+
fsa_feedback=_build_feedback(
138+
summary,
139+
student_result.errors,
140+
[],
141+
None,
142+
params
143+
)
152144
)
153-
154-
# Step 2: Validate expected FSA (should not fail)
155-
expected_errors = is_valid_fsa(expected_fsa)
156-
if expected_errors:
145+
146+
# -------------------------------------------------------------------------
147+
# Step 2: Validate expected FSA (should never fail)
148+
# -------------------------------------------------------------------------
149+
expected_result = is_valid_fsa(expected_fsa)
150+
if not expected_result.ok():
157151
return Result(
158152
is_correct=False,
159153
feedback="Oops! There's an issue with the expected answer. Please contact your instructor."
160154
)
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
155+
156+
# -------------------------------------------------------------------------
157+
# Step 3: Enforce expected automaton type
158+
# -------------------------------------------------------------------------
159+
if params.expected_type == "DFA":
160+
det_result = is_deterministic(student_fsa)
161+
if not det_result.ok():
162+
summary = "Your automaton must be deterministic (a DFA)."
163+
return Result(
164+
is_correct=False,
165+
feedback=summary,
166+
fsa_feedback=_build_feedback(
167+
summary,
168+
det_result.errors,
169+
[],
170+
None,
171+
params
172+
)
173+
)
174+
175+
# -------------------------------------------------------------------------
176+
# Step 4: Optional completeness check
177+
# -------------------------------------------------------------------------
178+
if params.check_completeness:
179+
comp_result = is_complete(student_fsa)
180+
if not comp_result.ok():
181+
validation_errors.extend(comp_result.errors)
182+
183+
# -------------------------------------------------------------------------
184+
# Step 5: Optional minimality check
185+
# -------------------------------------------------------------------------
186+
if params.check_minimality:
187+
min_errors = is_minimal(student_fsa)
188+
validation_errors.extend(min_errors)
189+
190+
# -------------------------------------------------------------------------
191+
# Step 6: Structural analysis (for feedback only)
192+
# -------------------------------------------------------------------------
169193
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)
194+
195+
# -------------------------------------------------------------------------
196+
# Step 7: Language equivalence
197+
# -------------------------------------------------------------------------
198+
equivalence_result = fsas_accept_same_language(
199+
student_fsa, expected_fsa
200+
)
201+
equivalence_errors = equivalence_result.errors
202+
203+
# -------------------------------------------------------------------------
204+
# Step 8: Decide correctness based on evaluation mode
205+
# -------------------------------------------------------------------------
206+
if params.evaluation_mode == "strict":
207+
is_correct = not validation_errors and equivalence_result.ok()
208+
elif params.evaluation_mode == "lenient":
209+
is_correct = equivalence_result.ok()
210+
else: # partial # I dont know what the partial is meant for, always mark as incorrect?
211+
is_correct = False
212+
213+
# -------------------------------------------------------------------------
214+
# Step 9: Build summary
215+
# -------------------------------------------------------------------------
216+
if is_correct:
217+
feedback = (
218+
f"Correct! Your FSA with {len(student_fsa.states)} state(s) "
219+
"accepts exactly the right language. Well done!"
182220
)
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-
221+
summary = "Your FSA is correct!"
222+
else:
223+
summary = (
224+
_summarize_errors(equivalence_errors)
225+
if equivalence_errors
226+
else "Your FSA has some issues to address."
227+
)
228+
feedback = summary
229+
230+
# -------------------------------------------------------------------------
231+
# Step 10: Return result
232+
# -------------------------------------------------------------------------
188233
return Result(
189234
is_correct=is_correct,
190-
feedback=summary,
191-
fsa_feedback=_build_feedback(summary, validation_errors, equivalence_errors, structural_info)
235+
feedback=feedback,
236+
fsa_feedback=_build_feedback(
237+
summary,
238+
validation_errors,
239+
equivalence_errors,
240+
structural_info,
241+
params
242+
)
192243
)

0 commit comments

Comments
 (0)