# Execution simplifiee de FOIL sur ancestor
# FOIL cherche des regles de la forme : ancestor(x, y) :- body.
# Etape 1 : Essayer le litteral parent(x, y)
# Si ancestor(x, y) :- parent(x, y). couvre des positifs et pas de negatifs
def check_clause_covers(
body_lits: list[Literal],
target_pred: str,
target_args: tuple[str, ...],
constants: set[str],
background: set[tuple[str, tuple[str, ...]]],
positives: set[tuple[str, ...]],
negatives: set[tuple[str, ...]]
) -> tuple[set[tuple[str, ...]], set[tuple[str, ...]]]:
"""Trouve les exemples positifs et negatifs DISTINCTS couverts par une clause.
On enumere toutes les substitutions des variables (y compris les variables
intermediaires comme z), mais on dedoublonne sur le tuple-cible : un meme
exemple ancestor(x, y) couvert par plusieurs valeurs de z ne compte qu'une
fois. C'est indispensable pour que le gain FOIL compare des grandeurs
coherentes (couverture d'EXEMPLES) ; sinon un litteral comme parent(x, z),
qui multiplie les bindings, parait faussement meilleur que parent(x, y).
"""
vars_in_clause = set()
for lit in body_lits:
vars_in_clause |= set(lit.args)
for a in target_args:
vars_in_clause.add(a)
# uniquement les variables (minuscules), pas d'eventuelles constantes
vars_list = sorted(v for v in vars_in_clause if v[0].islower())
covered_pos: set[tuple[str, ...]] = set()
covered_neg: set[tuple[str, ...]] = set()
for const_combo in itertools.product(constants, repeat=len(vars_list)):
binding = dict(zip(vars_list, const_combo))
# Verifier le body
body_ok = all(evaluate_literal(lit, binding, background) for lit in body_lits)
if not body_ok:
continue
# Extraire les arguments cibles (dedoublonnage via le set)
target_vals = tuple(binding.get(a, a) for a in target_args)
if target_vals in positives:
covered_pos.add(target_vals)
elif target_vals in negatives:
covered_neg.add(target_vals)
return covered_pos, covered_neg
print("FOIL --- Recherche de regles pour ancestor(x, y)")
print("=" * 55)
print()
# Clause candidate 1 : ancestor(x, y) :- parent(x, y).
body1 = [Literal("parent", ("x", "y"))]
cp1, cn1 = check_clause_covers(
body1, "ancestor", ("x", "y"), CONSTANTS, BACKGROUND, POSITIVES, NEGATIVES
)
print(f"Clause : ancestor(x, y) :- parent(x, y).")
print(f" Couvre {len(cp1)} positifs : {sorted(cp1)[:5]}...")
print(f" Couvre {len(cn1)} negatifs : {sorted(cn1)[:5]}...")
print(f" Gain FOIL : {foil_gain(len(POSITIVES), len(NEGATIVES), len(cp1), len(cn1)):.3f}")
print()
# Clause candidate 2 : ancestor(x, y) :- parent(x, z), parent(z, y).
body2 = [Literal("parent", ("x", "z")), Literal("parent", ("z", "y"))]
cp2, cn2 = check_clause_covers(
body2, "ancestor", ("x", "y"), CONSTANTS, BACKGROUND, POSITIVES, NEGATIVES
)
print(f"Clause : ancestor(x, y) :- parent(x, z), parent(z, y).")
print(f" Couvre {len(cp2)} positifs : {sorted(cp2)[:5]}...")
print(f" Couvre {len(cn2)} negatifs : {sorted(cn2)[:5]}...")
print(f" Gain FOIL : {foil_gain(len(POSITIVES), len(NEGATIVES), len(cp2), len(cn2)):.3f}")
print()
# Clause candidate 3 (recursive) : ancestor(x, y) :- parent(x, z), ancestor(z, y).
# Pour evaluer une regle recursive, on a besoin d'une definition (extension)
# de ancestor. Ici on illustre avec les seuls ancetres DIRECTS deja connus :
bg_with_direct_ancestors = BACKGROUND | {
("ancestor", pair) for pair in POSITIVES
if pair in {("Arthur", "Bob"), ("Arthur", "Catherine"),
("Bob", "Diana"), ("Catherine", "Eve"), ("Eve", "Frank")}
}
body3 = [Literal("parent", ("x", "z")), Literal("ancestor", ("z", "y"))]
cp3, cn3 = check_clause_covers(
body3, "ancestor", ("x", "y"), CONSTANTS, bg_with_direct_ancestors,
POSITIVES, NEGATIVES
)
print(f"Clause : ancestor(x, y) :- parent(x, z), ancestor(z, y). [recursive]")
print(f" Couvre {len(cp3)} positifs : {sorted(cp3)[:5]}...")
print(f" Couvre {len(cn3)} negatifs : {sorted(cn3)[:5]}...")
print(f" Gain FOIL : {foil_gain(len(POSITIVES), len(NEGATIVES), len(cp3), len(cn3)):.3f}")