Skip to content

Commit b071a80

Browse files
Merge pull request #12 from JohnnyWan1123/correction
FSA Correction, overall backend pipeline
2 parents ee9f99a + a691681 commit b071a80

10 files changed

Lines changed: 555 additions & 237 deletions

File tree

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# FSA Correction Module
2+
3+
Compares student FSAs against expected FSAs, returns `Result` with `FSAFeedback`.
4+
5+
## Exports
6+
7+
```python
8+
from evaluation_function.correction import (
9+
analyze_fsa_correction, # Main pipeline -> Result
10+
check_minimality, # Check if FSA is minimal
11+
)
12+
```
13+
14+
## Pipeline
15+
16+
```
17+
analyze_fsa_correction(student_fsa, expected_fsa, require_minimal=False) -> Result
18+
├── 1. Validate student FSA (is_valid_fsa)
19+
├── 2. Check minimality (if required)
20+
├── 3. Structural analysis (get_structured_info_of_fsa)
21+
└── 4. Language equivalence (fsas_accept_same_language -> are_isomorphic)
22+
└── All "why" feedback comes from here
23+
```
24+
25+
## Result
26+
27+
```python
28+
Result(
29+
is_correct: bool, # True if FSAs accept same language
30+
feedback: str, # Human-readable summary
31+
fsa_feedback: FSAFeedback # Structured feedback for UI
32+
)
33+
```
34+
35+
## Usage
36+
37+
```python
38+
from evaluation_function.correction import analyze_fsa_correction
39+
40+
result = analyze_fsa_correction(student_fsa, expected_fsa)
41+
result.is_correct # bool
42+
result.feedback # "Correct!" or "Languages differ: ..."
43+
result.fsa_feedback.errors # List[ValidationError] with ElementHighlight
44+
```
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"""FSA Correction Module
2+
3+
Compares student FSAs against expected FSAs.
4+
5+
Exports:
6+
- analyze_fsa_correction: Main pipeline (FSA vs FSA -> Result)
7+
- check_minimality: Check if FSA is minimal
8+
"""
9+
10+
from .correction import (
11+
analyze_fsa_correction,
12+
check_minimality,
13+
)
14+
15+
__all__ = [
16+
"analyze_fsa_correction",
17+
"check_minimality",
18+
]
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
"""
2+
FSA Correction Module
3+
4+
Compares student FSAs against expected FSAs.
5+
Returns Result with FSAFeedback for UI highlighting.
6+
7+
All detailed "why" feedback comes from are_isomorphic() in validation module.
8+
"""
9+
10+
from typing import List, Optional, Tuple
11+
12+
# Schema imports
13+
from ..schemas import FSA, ValidationError, ErrorCode
14+
from ..schemas.result import Result, FSAFeedback, StructuralInfo, LanguageComparison
15+
16+
# Validation imports
17+
from ..validation.validation import (
18+
is_valid_fsa,
19+
fsas_accept_same_language,
20+
get_structured_info_of_fsa,
21+
)
22+
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+
return False, ValidationError(
37+
message=f"FSA is not minimal: has {len(fsa.states)} states but can be reduced to {len(minimized.states)}",
38+
code=ErrorCode.NOT_MINIMAL,
39+
severity="error",
40+
suggestion="Minimize your FSA by merging equivalent states"
41+
)
42+
return True, None
43+
except Exception:
44+
return True, None
45+
46+
47+
def check_minimality(fsa: FSA) -> bool:
48+
"""Check if FSA is minimal."""
49+
is_min, _ = _check_minimality(fsa)
50+
return is_min
51+
52+
53+
# =============================================================================
54+
# Helper Functions
55+
# =============================================================================
56+
57+
def _build_feedback(
58+
summary: str,
59+
validation_errors: List[ValidationError],
60+
equivalence_errors: List[ValidationError],
61+
structural_info: Optional[StructuralInfo]
62+
) -> FSAFeedback:
63+
"""Build FSAFeedback from errors and analysis."""
64+
all_errors = validation_errors + equivalence_errors
65+
errors = [e for e in all_errors if e.severity == "error"]
66+
warnings = [e for e in all_errors if e.severity in ("warning", "info")]
67+
68+
# Build hints from all error suggestions
69+
hints = [e.suggestion for e in all_errors if e.suggestion]
70+
if structural_info:
71+
if structural_info.unreachable_states:
72+
hints.append("Consider removing unreachable states")
73+
if structural_info.dead_states:
74+
hints.append("Dead states can never lead to acceptance")
75+
76+
# Build language comparison
77+
language = LanguageComparison(are_equivalent=len(equivalence_errors) == 0)
78+
79+
return FSAFeedback(
80+
summary=summary,
81+
errors=errors,
82+
warnings=warnings,
83+
structural=structural_info,
84+
language=language,
85+
hints=hints
86+
)
87+
88+
89+
def _summarize_errors(errors: List[ValidationError]) -> str:
90+
"""Generate summary from error messages."""
91+
error_types = set()
92+
for error in errors:
93+
msg = error.message.lower()
94+
if "alphabet" in msg:
95+
error_types.add("alphabet mismatch")
96+
elif "state" in msg and "count" in msg:
97+
error_types.add("state count mismatch")
98+
elif "accepting" in msg or "incorrectly marked" in msg:
99+
error_types.add("acceptance error")
100+
elif "transition" in msg:
101+
error_types.add("transition error")
102+
103+
if error_types:
104+
return f"Languages differ: {', '.join(error_types)}"
105+
return f"Languages differ: {len(errors)} issue(s)"
106+
107+
108+
# =============================================================================
109+
# Main Pipeline
110+
# =============================================================================
111+
112+
def analyze_fsa_correction(
113+
student_fsa: FSA,
114+
expected_fsa: FSA,
115+
require_minimal: bool = False
116+
) -> Result:
117+
"""
118+
Compare student FSA against expected FSA.
119+
120+
Returns Result with:
121+
- is_correct: True if FSAs accept same language
122+
- feedback: Human-readable summary
123+
- fsa_feedback: Structured feedback with ElementHighlight for UI
124+
125+
Args:
126+
student_fsa: The student's FSA
127+
expected_fsa: The reference/expected FSA
128+
require_minimal: Whether to require student FSA to be minimal
129+
"""
130+
validation_errors: List[ValidationError] = []
131+
equivalence_errors: List[ValidationError] = []
132+
structural_info: Optional[StructuralInfo] = None
133+
134+
# Step 1: Validate student FSA structure
135+
student_errors = is_valid_fsa(student_fsa)
136+
if student_errors:
137+
summary = "FSA has structural errors"
138+
return Result(
139+
is_correct=False,
140+
feedback=summary,
141+
fsa_feedback=_build_feedback(summary, student_errors, [], None)
142+
)
143+
144+
# Step 2: Validate expected FSA (should not fail)
145+
expected_errors = is_valid_fsa(expected_fsa)
146+
if expected_errors:
147+
return Result(
148+
is_correct=False,
149+
feedback="Internal error: expected FSA is invalid"
150+
)
151+
152+
# Step 3: Check minimality if required
153+
if require_minimal:
154+
is_min, min_error = _check_minimality(student_fsa)
155+
if not is_min and min_error:
156+
validation_errors.append(min_error)
157+
158+
# Step 4: Structural analysis
159+
structural_info = get_structured_info_of_fsa(student_fsa)
160+
161+
# Step 5: Language equivalence (with detailed feedback from are_isomorphic)
162+
equivalence_errors = fsas_accept_same_language(student_fsa, expected_fsa)
163+
164+
if not equivalence_errors and not validation_errors:
165+
return Result(
166+
is_correct=True,
167+
feedback="Correct! FSA accepts the expected language.",
168+
fsa_feedback=_build_feedback("FSA is correct", [], [], structural_info)
169+
)
170+
171+
# Build result with errors
172+
is_correct = len(equivalence_errors) == 0 and len(validation_errors) == 0
173+
summary = _summarize_errors(equivalence_errors) if equivalence_errors else "FSA has issues"
174+
175+
return Result(
176+
is_correct=is_correct,
177+
feedback=summary,
178+
fsa_feedback=_build_feedback(summary, validation_errors, equivalence_errors, structural_info)
179+
)

evaluation_function/evaluation.py

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,46 @@
11
from typing import Any
2-
from lf_toolkit.evaluation import Result, Params
2+
from lf_toolkit.evaluation import Result as LFResult, Params
3+
4+
from .schemas import FSA
5+
from .schemas.result import Result
6+
from .correction import analyze_fsa_correction
7+
38

49
def evaluation_function(
510
response: Any,
611
answer: Any,
712
params: Params,
8-
) -> Result:
13+
) -> LFResult:
914
"""
10-
Function used to evaluate a student response.
11-
---
12-
The handler function passes three arguments to evaluation_function():
13-
14-
- `response` which are the answers provided by the student.
15-
- `answer` which are the correct answers to compare against.
16-
- `params` which are any extra parameters that may be useful,
17-
e.g., error tolerances.
18-
19-
The output of this function is what is returned as the API response
20-
and therefore must be JSON-encodable. It must also conform to the
21-
response schema.
22-
23-
Any standard python library may be used, as well as any package
24-
available on pip (provided it is added to requirements.txt).
25-
26-
The way you wish to structure you code (all in this function, or
27-
split into many) is entirely up to you. All that matters are the
28-
return types and that evaluation_function() is the main function used
29-
to output the evaluation response.
15+
Evaluate a student's FSA response against the expected answer.
16+
17+
Args:
18+
response: Student's FSA (dict with states, alphabet, transitions, etc.)
19+
answer: Expected FSA
20+
params: Extra parameters (e.g., require_minimal)
21+
22+
Returns:
23+
LFResult with is_correct and feedback
3024
"""
31-
32-
return Result(
33-
is_correct=response == answer
34-
)
25+
try:
26+
# Parse FSAs from input
27+
student_fsa = FSA.model_validate(response)
28+
expected_fsa = FSA.model_validate(answer)
29+
30+
# Get require_minimal from params if present
31+
require_minimal = params.get("require_minimal", False) if hasattr(params, "get") else False
32+
33+
# Run correction pipeline
34+
result: Result = analyze_fsa_correction(student_fsa, expected_fsa, require_minimal)
35+
36+
# Convert to lf_toolkit Result
37+
return LFResult(
38+
is_correct=result.is_correct,
39+
feedback_items=[("feedback", result.feedback)]
40+
)
41+
42+
except Exception as e:
43+
return LFResult(
44+
is_correct=False,
45+
feedback_items=[("error", f"Invalid FSA format: {str(e)}")]
46+
)

0 commit comments

Comments
 (0)