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
1315from ..schemas import FSA , ValidationError , ErrorCode
1618# Validation imports
1719from ..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
5833def _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
9285def _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:
118111def 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