77All 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
1416from ..schemas .result import Result , FSAFeedback , StructuralInfo , LanguageComparison
1517
1618# Validation imports
1719from ..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
5834def _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
9286def _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:
118112def 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