diff --git a/evaluation_function/correction/correction.py b/evaluation_function/correction/correction.py index e0e6d27..20c3242 100644 --- a/evaluation_function/correction/correction.py +++ b/evaluation_function/correction/correction.py @@ -12,11 +12,12 @@ from evaluation_function.schemas.params import Params # Schema imports -from ..schemas import FSA, ValidationError, ErrorCode +from ..schemas import FSA, ValidationError, ErrorCode, ValidationResult from ..schemas.result import Result, FSAFeedback, StructuralInfo, LanguageComparison # Validation imports from ..validation.validation import ( + are_isomorphic, is_valid_fsa, is_deterministic, is_complete, @@ -125,7 +126,7 @@ def analyze_fsa_correction( # Step 1: Validate student FSA structure # ------------------------------------------------------------------------- student_result = is_valid_fsa(student_fsa) - if not student_result.ok(): + if not student_result.ok: summary = ( "Your FSA has a structural problem that needs to be fixed first." if len(student_result.errors) == 1 @@ -147,7 +148,7 @@ def analyze_fsa_correction( # Step 2: Validate expected FSA (should never fail) # ------------------------------------------------------------------------- expected_result = is_valid_fsa(expected_fsa) - if not expected_result.ok(): + if not expected_result.ok: return Result( is_correct=False, feedback="Oops! There's an issue with the expected answer. Please contact your instructor." @@ -158,7 +159,7 @@ def analyze_fsa_correction( # ------------------------------------------------------------------------- if params.expected_type == "DFA": det_result = is_deterministic(student_fsa) - if not det_result.ok(): + if not det_result.ok: summary = "Your automaton must be deterministic (a DFA)." return Result( is_correct=False, @@ -177,15 +178,17 @@ def analyze_fsa_correction( # ------------------------------------------------------------------------- if params.check_completeness: comp_result = is_complete(student_fsa) - if not comp_result.ok(): + if not comp_result.ok: validation_errors.extend(comp_result.errors) # ------------------------------------------------------------------------- # Step 5: Optional minimality check # ------------------------------------------------------------------------- + validation_result = None if params.check_minimality: - min_errors = is_minimal(student_fsa) - validation_errors.extend(min_errors) + validation_result = is_minimal(student_fsa) + if not validation_result.ok: + validation_errors.extend(validation_result.errors) # ------------------------------------------------------------------------- # Step 6: Structural analysis (for feedback only) @@ -198,20 +201,26 @@ def analyze_fsa_correction( equivalence_result = fsas_accept_same_language( student_fsa, expected_fsa ) - equivalence_errors = equivalence_result.errors + equivalence_errors.extend(equivalence_result.errors) # ------------------------------------------------------------------------- - # Step 8: Decide correctness based on evaluation mode + # Step 8: Isomorphism + # ------------------------------------------------------------------------- + iso_result = are_isomorphic(student_fsa, expected_fsa) + equivalence_errors.extend(iso_result.errors) + + # ------------------------------------------------------------------------- + # Step 9: Decide correctness based on evaluation mode # ------------------------------------------------------------------------- if params.evaluation_mode == "strict": - is_correct = not validation_errors and equivalence_result.ok() + is_correct = validation_result is not None and validation_result.ok and equivalence_result.ok and iso_result.ok elif params.evaluation_mode == "lenient": - is_correct = equivalence_result.ok() + is_correct = validation_result is not None and validation_result.ok and equivalence_result.ok else: # partial # I dont know what the partial is meant for, always mark as incorrect? is_correct = False # ------------------------------------------------------------------------- - # Step 9: Build summary + # Step 10: Build summary # ------------------------------------------------------------------------- if is_correct: feedback = ( @@ -226,9 +235,10 @@ def analyze_fsa_correction( else "Your FSA has some issues to address." ) feedback = summary + print(equivalence_errors) # ------------------------------------------------------------------------- - # Step 10: Return result + # Step 11: Return result # ------------------------------------------------------------------------- return Result( is_correct=is_correct, diff --git a/evaluation_function/test/test_correction.py b/evaluation_function/test/test_correction.py index f3fefa5..88c3c4d 100644 --- a/evaluation_function/test/test_correction.py +++ b/evaluation_function/test/test_correction.py @@ -8,11 +8,12 @@ from evaluation_function.schemas import ValidationError, ErrorCode from evaluation_function.schemas.utils import make_fsa from evaluation_function.schemas.result import Result, FSAFeedback +from evaluation_function.schemas.params import Params from evaluation_function.correction import analyze_fsa_correction # ============================================================================= -# Fixtures +# Fixtures - DFAs # ============================================================================= @pytest.fixture @@ -72,6 +73,23 @@ def equivalent_dfa(): ) +# ============================================================================= +# Helper: Default Params +# ============================================================================= + +@pytest.fixture +def default_params(): + """Default Params object for analyze_fsa_correction.""" + return Params( + expected_type="DFA", + check_completeness=True, + check_minimality=True, + evaluation_mode="strict", + highlight_errors=True, + feedback_verbosity="detailed" + ) + + # ============================================================================= # Test Main Pipeline - Returns Result # ============================================================================= @@ -79,29 +97,30 @@ def equivalent_dfa(): class TestAnalyzeFsaCorrection: """Test the main analysis pipeline returns Result.""" - def test_equivalent_fsas_correct(self, dfa_accepts_a, equivalent_dfa): - result = analyze_fsa_correction(dfa_accepts_a, equivalent_dfa) + def test_equivalent_fsas_correct(self, dfa_accepts_a, equivalent_dfa, default_params): + result = analyze_fsa_correction(dfa_accepts_a, equivalent_dfa, default_params) + print(result) assert isinstance(result, Result) assert result.is_correct is True assert "Correct" in result.feedback - def test_different_fsas_incorrect(self, dfa_accepts_a, dfa_accepts_a_or_b): - result = analyze_fsa_correction(dfa_accepts_a, dfa_accepts_a_or_b) + def test_different_fsas_incorrect(self, dfa_accepts_a, dfa_accepts_a_or_b, default_params): + result = analyze_fsa_correction(dfa_accepts_a, dfa_accepts_a_or_b, default_params) assert isinstance(result, Result) assert result.is_correct is False - def test_result_has_fsa_feedback(self, dfa_accepts_a, equivalent_dfa): - result = analyze_fsa_correction(dfa_accepts_a, equivalent_dfa) + def test_result_has_fsa_feedback(self, dfa_accepts_a, equivalent_dfa, default_params): + result = analyze_fsa_correction(dfa_accepts_a, equivalent_dfa, default_params) assert result.fsa_feedback is not None assert isinstance(result.fsa_feedback, FSAFeedback) - def test_fsa_feedback_has_structural_info(self, dfa_accepts_a, equivalent_dfa): - result = analyze_fsa_correction(dfa_accepts_a, equivalent_dfa) + def test_fsa_feedback_has_structural_info(self, dfa_accepts_a, equivalent_dfa, default_params): + result = analyze_fsa_correction(dfa_accepts_a, equivalent_dfa, default_params) assert result.fsa_feedback.structural is not None assert result.fsa_feedback.structural.num_states == 3 - def test_different_fsas_have_errors(self, dfa_accepts_a, dfa_accepts_a_or_b): - result = analyze_fsa_correction(dfa_accepts_a, dfa_accepts_a_or_b) + def test_different_fsas_have_errors(self, dfa_accepts_a, dfa_accepts_a_or_b, default_params): + result = analyze_fsa_correction(dfa_accepts_a, dfa_accepts_a_or_b, default_params) assert result.fsa_feedback is not None assert len(result.fsa_feedback.errors) > 0 @@ -113,7 +132,7 @@ def test_different_fsas_have_errors(self, dfa_accepts_a, dfa_accepts_a_or_b): class TestInvalidFsas: """Test handling of invalid FSAs.""" - def test_invalid_initial_state(self): + def test_invalid_initial_state(self, default_params): invalid = make_fsa( states=["q0"], alphabet=["a"], @@ -121,12 +140,12 @@ def test_invalid_initial_state(self): initial="invalid", accept=[] ) - result = analyze_fsa_correction(invalid, invalid) + result = analyze_fsa_correction(invalid, invalid, default_params) assert result.is_correct is False assert result.fsa_feedback is not None assert len(result.fsa_feedback.errors) > 0 - def test_invalid_accept_state(self): + def test_invalid_accept_state(self, default_params): invalid = make_fsa( states=["q0"], alphabet=["a"], @@ -134,15 +153,27 @@ def test_invalid_accept_state(self): initial="q0", accept=["invalid"] ) - result = analyze_fsa_correction(invalid, invalid) + result = analyze_fsa_correction(invalid, invalid, default_params) assert result.is_correct is False +# ============================================================================= +# Test Minimality +# ============================================================================= + class TestAnalyzeFsaCorrectionMinimality: """Test analyze_fsa_correction with minimality checking.""" def test_minimal_fsa_passes(self, dfa_accepts_a, equivalent_dfa): - result = analyze_fsa_correction(dfa_accepts_a, equivalent_dfa, require_minimal=True) + params = Params( + expected_type="DFA", + check_completeness=True, + check_minimality=True, + evaluation_mode="strict", + highlight_errors=True, + feedback_verbosity="detailed" + ) + result = analyze_fsa_correction(dfa_accepts_a, equivalent_dfa, params) assert result.is_correct is True def test_non_minimal_fsa_fails_when_required(self, equivalent_dfa): @@ -162,9 +193,18 @@ def test_non_minimal_fsa_fails_when_required(self, equivalent_dfa): initial="q0", accept=["q1"] ) - result = analyze_fsa_correction(non_minimal, equivalent_dfa, require_minimal=True) + params = Params( + expected_type="DFA", + check_completeness=True, + check_minimality=True, + evaluation_mode="strict", + highlight_errors=True, + feedback_verbosity="detailed" + ) + result = analyze_fsa_correction(non_minimal, equivalent_dfa, params) # Should have minimality error assert result.fsa_feedback is not None + assert any(e.code == ErrorCode.NOT_MINIMAL for e in result.fsa_feedback.errors) if __name__ == "__main__": diff --git a/evaluation_function/test/test_validation.py b/evaluation_function/test/test_validation.py index e843810..3cf711a 100644 --- a/evaluation_function/test/test_validation.py +++ b/evaluation_function/test/test_validation.py @@ -23,7 +23,7 @@ def test_valid_fsa_basic(self): initial="q0", accept=["q1"], ) - assert is_valid_fsa(fsa).ok() + assert is_valid_fsa(fsa).ok def test_invalid_initial_state(self): fsa = make_fsa( @@ -34,7 +34,7 @@ def test_invalid_initial_state(self): accept=[], ) result = is_valid_fsa(fsa) - assert not result.ok() + assert not result.ok assert ErrorCode.INVALID_INITIAL in [e.code for e in result.errors] def test_invalid_accept_state(self): @@ -46,7 +46,7 @@ def test_invalid_accept_state(self): accept=["q1"], ) result = is_valid_fsa(fsa) - assert not result.ok() + assert not result.ok assert ErrorCode.INVALID_ACCEPT in [e.code for e in result.errors] def test_invalid_transition_source(self): @@ -99,7 +99,7 @@ def test_deterministic_fsa(self): initial="q0", accept=["q1"], ) - assert is_deterministic(fsa).ok() + assert is_deterministic(fsa).ok def test_nondeterministic_fsa(self): fsa = make_fsa( @@ -113,7 +113,7 @@ def test_nondeterministic_fsa(self): accept=["q2"], ) result = is_deterministic(fsa) - assert not result.ok() + assert not result.ok assert ErrorCode.DUPLICATE_TRANSITION in [e.code for e in result.errors] @@ -133,7 +133,7 @@ def test_complete_dfa(self): initial="q0", accept=["q1"], ) - assert is_complete(fsa).ok() + assert is_complete(fsa).ok def test_incomplete_dfa(self): fsa = make_fsa( @@ -144,7 +144,7 @@ def test_incomplete_dfa(self): accept=["q1"], ) result = is_complete(fsa) - assert not result.ok() + assert not result.ok assert ErrorCode.MISSING_TRANSITION in [e.code for e in result.errors] def test_complete_requires_deterministic(self): @@ -176,7 +176,7 @@ def test_find_unreachable_states(self): accept=["q1"], ) result = find_unreachable_states(fsa) - assert not result.ok() + assert not result.ok assert ErrorCode.UNREACHABLE_STATE in [e.code for e in result.errors] def test_find_dead_states(self): @@ -193,7 +193,7 @@ def test_find_dead_states(self): accept=["q1"], ) result = find_dead_states(fsa) - assert not result.ok() + assert not result.ok assert ErrorCode.DEAD_STATE in [e.code for e in result.errors] def test_dead_states_no_accept_states(self): @@ -222,7 +222,7 @@ def test_accepts_string(self): initial="q0", accept=["q1"], ) - assert accepts_string(fsa, "a").ok() + assert accepts_string(fsa, "a").ok def test_rejected_no_transition(self): fsa = make_fsa( @@ -243,7 +243,7 @@ def test_empty_string(self): initial="q0", accept=["q0"], ) - assert accepts_string(fsa, "").ok() + assert accepts_string(fsa, "").ok class TestLanguageEquivalence: @@ -264,7 +264,7 @@ def test_accept_same_string(self): initial="s0", accept=["s1"], ) - assert fsas_accept_same_string(fsa1, fsa2, "a") == [] + assert fsas_accept_same_string(fsa1, fsa2, "a").ok def test_language_mismatch(self): fsa1 = make_fsa( @@ -281,8 +281,9 @@ def test_language_mismatch(self): initial="s0", accept=["s0"], ) - errors = fsas_accept_same_language(fsa1, fsa2) - assert ErrorCode.LANGUAGE_MISMATCH in [e.code for e in errors] + result = fsas_accept_same_language(fsa1, fsa2) + assert not result.ok + assert ErrorCode.LANGUAGE_MISMATCH in [e.code for e in result.errors] class TestIsomorphism: @@ -313,7 +314,8 @@ def test_isomorphic_dfas(self): initial="s0", accept=["s1"], ) - assert are_isomorphic(fsa_user, fsa_sol) == [] + assert are_isomorphic(fsa_user, fsa_sol).ok + # ============================================================================= # Test Minimality @@ -337,11 +339,14 @@ def dfa_accepts_a(): accept=["q1"] ) + class TestCheckMinimality: """Test check_minimality function.""" def test_minimal_dfa(self, dfa_accepts_a): - assert is_minimal(dfa_accepts_a) is True + result = is_minimal(dfa_accepts_a) + assert result.ok + assert result.value is True def test_non_minimal_dfa_with_unreachable(self): non_minimal = make_fsa( @@ -359,8 +364,5 @@ def test_non_minimal_dfa_with_unreachable(self): initial="q0", accept=["q1"] ) - assert is_minimal(non_minimal) is False - - -if __name__ == "__main__": - pytest.main([__file__, "-v"]) + result = is_minimal(non_minimal) + assert not result.ok diff --git a/evaluation_function/validation/validation.py b/evaluation_function/validation/validation.py index e6a8b20..7cc3bbe 100644 --- a/evaluation_function/validation/validation.py +++ b/evaluation_function/validation/validation.py @@ -359,26 +359,102 @@ def fsas_accept_same_language(fsa1: FSA, fsa2: FSA) -> ValidationResult[bool]: def are_isomorphic(fsa1: FSA, fsa2: FSA) -> ValidationResult[bool]: - errors: List[ValidationError] = [] - + """ + Checks if two DFAs are isomorphic. + Returns a list of ValidationErrors if they differ, otherwise an empty list. + Assumes DFAs are minimized and complete. + """ + errors = [] + # 1. Alphabet Check (Mandatory) if set(fsa1.alphabet) != set(fsa2.alphabet): errors.append( ValidationError( - message="Alphabets do not match.", + message="The alphabet of your FSA does not match the required alphabet.", code=ErrorCode.LANGUAGE_MISMATCH, - severity="error" + severity="error", + suggestion=f"Your alphabet: {set(fsa1.alphabet)}. Expected: {set(fsa2.alphabet)}." ) ) + # 2. Basic Structural Check (State Count) if len(fsa1.states) != len(fsa2.states): errors.append( ValidationError( - message="State counts do not match.", + message=f"FSA structure mismatch: expected {len(fsa2.states)} states, but found {len(fsa1.states)}.", code=ErrorCode.LANGUAGE_MISMATCH, - severity="error" + severity="error", + suggestion="Verify if you have unnecessary states or if you have minimized your FSA." ) ) + # 3. State Mapping Initialization + mapping: Dict[str, str] = {fsa1.initial_state: fsa2.initial_state} + queue = deque([fsa1.initial_state]) + visited = {fsa1.initial_state} + + # Optimization: Pre-map transitions + trans1 = {(t.from_state, t.symbol): t.to_state for t in fsa1.transitions} + trans2 = {(t.from_state, t.symbol): t.to_state for t in fsa2.transitions} + accept1 = set(fsa1.accept_states) + accept2 = set(fsa2.accept_states) + + while queue: + s1 = queue.popleft() + s2 = mapping[s1] + + # 4. Check Acceptance Parity + if (s1 in accept1) != (s2 in accept2): + expected_type = "accepting" if s2 in accept2 else "non-accepting" + errors.append( + ValidationError( + message=f"State '{s1}' is incorrectly marked. It should be an {expected_type} state.", + code=ErrorCode.LANGUAGE_MISMATCH, + severity="error", + highlight=ElementHighlight(type="state", state_id=s1), + suggestion=f"Toggle the 'accept' status of state '{s1}'." + ) + ) + + # 5. Check Transitions for every symbol in the shared alphabet + for symbol in fsa1.alphabet: + dest1 = trans1.get((s1, symbol)) + dest2 = trans2.get((s2, symbol)) + + # Missing Transition Check + if (dest1 is None) != (dest2 is None): + errors.append( + ValidationError( + message=f"Missing or extra transition from state '{s1}' on symbol '{symbol}'.", + code=ErrorCode.LANGUAGE_MISMATCH, + severity="error", + highlight=ElementHighlight(type="state", state_id=s1, symbol=symbol), + suggestion="Ensure your DFA is complete and follows the transition logic." + ) + ) + + if dest1 is not None: + if dest1 not in mapping: + # New state discovered: check if we've exceeded state count in mapping + mapping[dest1] = dest2 + visited.add(dest1) + queue.append(dest1) + else: + # Consistency check: does fsa1 transition to the same logical state as fsa2? + if mapping[dest1] != dest2: + errors.append( + ValidationError( + message=f"Transition from '{s1}' on '{symbol}' leads to the wrong state.", + code=ErrorCode.LANGUAGE_MISMATCH, + severity="error", + highlight=ElementHighlight( + type="transition", + from_state=s1, + to_state=dest1, + symbol=symbol + ), + suggestion="Check if this transition should point to a different state." + ) + ) return ( ValidationResult.success(True) if not errors