Limitations connues (Tweety 1.28): - FOL avec égalité peut causer des problèmes de heap space avec SimpleFolReasoner - Préférer EProver pour les requêtes FOL complexes
# --- Initialisation JVM Tweety + Outils Externes ---print("--- Verification JVM Tweety + Outils ---")jvm_ready =Falseimport jpypeimport jpype.importsimport osimport pathlibimport shutilimport platform# === Configuration COMPLETE des outils externes ===EXTERNAL_TOOLS = {"CLINGO": "","SPASS": "","EPROVER": "", # Nouveau: prouveur FOL"SAT_SOLVER_PYTHON": "", # Nouveau: wrapper pySAT"MARCO": "", # Nouveau: MUS enumerator}def get_tool_path(tool_name):"""Retourne le chemin valide d'un outil ou None.""" path_str = EXTERNAL_TOOLS.get(tool_name, "")ifnot path_str: returnNoneif shutil.which(path_str):return path_str path_obj = pathlib.Path(path_str)if path_obj.is_file():returnstr(path_obj.resolve())if path_obj.is_dir():returnstr(path_obj.resolve())returnNone# --- Auto-detection des outils ---system = platform.system()exe_suffix =".exe"if system =="Windows"else""# 1. Clingo (ASP solver)for cp in [shutil.which("clingo"), pathlib.Path(f"ext_tools/clingo/clingo{exe_suffix}"), pathlib.Path(f"../ext_tools/clingo/clingo{exe_suffix}")]:if cp and (isinstance(cp, str) or cp.exists()): EXTERNAL_TOOLS["CLINGO"] =str(pathlib.Path(cp).parent.resolve()) ifisinstance(cp, pathlib.Path) elsestr(pathlib.Path(cp).parent)break# 2. SPASS (Modal logic prover)for sp in [shutil.which("SPASS"), pathlib.Path(f"ext_tools/spass/SPASS{exe_suffix}"), pathlib.Path(f"../ext_tools/spass/SPASS{exe_suffix}")]:if sp and (isinstance(sp, str) or sp.exists()): EXTERNAL_TOOLS["SPASS"] =str(pathlib.Path(sp).resolve()) ifisinstance(sp, pathlib.Path) else spbreak# 3. EProver (FOL theorem prover) - NOUVEAUfor ep in [shutil.which("eprover"), pathlib.Path(f"../ext_tools/EProver/eprover{exe_suffix}"), pathlib.Path(f"../../ext_tools/EProver/eprover{exe_suffix}")]:if ep: ep_path = pathlib.Path(ep) ifisinstance(ep, str) else epif ep_path.exists(): EXTERNAL_TOOLS["EPROVER"] =str(ep_path.resolve())break# 4. SAT Solver Python (CaDiCaL, Glucose via pySAT) - NOUVEAUfor sat in [pathlib.Path("../ext_tools/sat_solver.py"), pathlib.Path("../../ext_tools/sat_solver.py")]:if sat.exists(): EXTERNAL_TOOLS["SAT_SOLVER_PYTHON"] =str(sat.resolve())break# 5. MARCO (MUS enumerator) - NOUVEAUfor mp in [pathlib.Path("../ext_tools/marco.py"), pathlib.Path("../../ext_tools/marco.py")]:if mp.exists(): EXTERNAL_TOOLS["MARCO"] =str(mp.resolve())break# === Initialisation JVM ===if jpype.isJVMStarted():print("JVM deja en cours d'execution.") jvm_ready =Trueelse:# Chercher JDK portable jdk_portable =Nonefor jdk_path in [pathlib.Path("jdk-17-portable"), pathlib.Path("../Argument_Analysis/jdk-17-portable")]:if jdk_path.exists(): zulu_dirs =list(jdk_path.glob("zulu*"))if zulu_dirs: jdk_portable = zulu_dirs[0] os.environ["JAVA_HOME"] =str(jdk_portable.resolve())print(f"JDK portable: {jdk_portable.name}")breakifnot os.environ.get("JAVA_HOME"):print("ERREUR: JAVA_HOME non defini et JDK portable non trouve.")else: LIB_DIR = pathlib.Path("libs")ifnot LIB_DIR.exists(): LIB_DIR = pathlib.Path("../Argument_Analysis/libs")if LIB_DIR.exists(): jar_files =list(LIB_DIR.glob("*.jar"))if jar_files: classpath = os.pathsep.join(str(j.resolve()) for j in jar_files)try: jpype.startJVM(classpath=[classpath])print(f"JVM demarree avec {len(jar_files)} JARs.") jvm_ready =TrueexceptExceptionas e:print(f"Erreur demarrage JVM: {e}")# === Resume des outils ===if jvm_ready:print("\n--- Outils disponibles ---") tools_status = []for tool, path in EXTERNAL_TOOLS.items():if path: short_path = path.split(os.sep)[-1] iflen(path) >30else path tools_status.append(f"{tool}: OK")print(f" {tool}: {short_path}")else: tools_status.append(f"{tool}: -")print(f"\nJVM prete. Outils: {sum(1for t,p in EXTERNAL_TOOLS.items() if p)}/{len(EXTERNAL_TOOLS)}")
Succes de l’initialisation : - JVM demarree avec 10 JARs charges - JDK portable Zulu 17 detecte et utilise - 3/5 outils externes detectes (EPROVER, SAT_SOLVER_PYTHON, MARCO)
Outils externes (cartographie des roles ; 3/5 detectes sur ce run, cf cellule precedente) :
Outil
Rôle
Utilisation
CLINGO
Solveur ASP (Answer Set Programming)
Logique ASP (notebook 6)
SPASS
Prouveur de theoremes modal
Logique modale (notebook 3)
EPROVER
Prouveur FOL (First-Order Logic)
Logique du premier ordre (section 2.2)
SAT_SOLVER_PYTHON
Wrapper pySAT (CaDiCaL, Glucose)
Solveurs SAT modernes (section 2.1.3)
MARCO
Enumerateur MUS (Minimal Unsatisfiable Subsets)
Analyse d’incohérences (notebook 4)
Importance de la detection automatique : - Les outils sont cherches dans ext_tools/ et dans le PATH système - Si un outil manque, Tweety utilise un fallback (ex: SAT4J au lieu de CaDiCaL) - Cette approche permet de fonctionner partout tout en beneficiant d’outils externes quand disponibles
Note : Pour les notebooks suivants, EProver et pySAT seront particulierement utiles.
Note de parite cross-langage (EPIC #4956) : Le jumeau C# de ce notebook (voir Tweety-02-Basic-Logics-CSharp.ipynb) utilise une strategie differente : IKVM transpile statiquement le bytecode Java de Tweety en DLL .NET native (7 Mo), tandis que ce notebook Python utilise jpype pour demarrer une JVM in-process et appeler directement les classes Java. Les deux strategies donnent acces au meme API Tweety (Proposition, PlBeliefSet, PlParser, FolReasoner…). Audit c.739 (2026-07-22) : 0 doc-honesty finding corrigible des deux cotes.
Partie 2 : Logiques Fondamentales dans Tweety
Explorons comment représenter et raisonner avec certaines logiques de base en utilisant Tweety via JPype.
2.1 Logique Propositionnelle (PL)
La logique propositionnelle est la fondation de nombreux systèmes de raisonnement. Elle permet de représenter des faits et des relations logiques entre eux.
Pourquoi la logique propositionnelle ? - Base de tous les systèmes de raisonnement automatique - Fondement des solveurs SAT (utilisés en vérification, planification, argumentation) - Suffisante pour modéliser de nombreux problèmes combinatoires
Concepts Clés :
Concept
Classe Tweety
Syntaxe
Exemple
Proposition
Proposition
lettre
a, pluie
Négation
Negation
!
!a = “non a”
Conjonction
Conjunction
&&
a && b = “a et b”
Disjonction
Disjunction
\|\|
a \|\| b = “a ou b”
Implication
Implication
=>
a => b = “si a alors b”
Équivalence
Equivalence
<=>
a <=> b = “a ssi b”
Xor
ExclusiveDisjunction
^^
a ^^ b = “a ou b mais pas les deux”
Tables de vérité (rappel):
a
b
a && b
a || b
a => b
!a
0
0
0
0
1
1
0
1
0
1
1
1
1
0
0
1
0
0
1
1
1
1
1
0
Classes principales: * PlFormula: Interface/classe de base pour toutes les formules PL * PlBeliefSet: Un ensemble de formules (base de connaissances) * PossibleWorld: Une assignation de vérité aux propositions (interprétation) * PlParser: Analyse syntaxique de chaînes vers formules * Raisonnement: SimplePlReasoner (énumération), SatSolver (SAT4J, etc.)
2.1.1 Syntaxe, Parsing, Mondes Possibles
Voyons comment créer, parser et évaluer des formules PL.
Succes : La JVM est prete et les classes PL ont ete chargees.
Classes disponibles : - Proposition, Negation, Conjunction, Disjunction, Implication : Constructeurs de formules - PlBeliefSet : Base de connaissances (ensemble de formules) - PlParser : Parsing de formules depuis des chaînes - PossibleWorld : Interpretations (assignations de verite)
Architecture Tweety pour PL :
PlSignature (vocabulaire : ensemble de propositions)
|
PlFormula (syntaxe : arbres de formules)
|
PlBeliefSet (base de connaissances)
|
PlReasoner (sémantique : inference logique)
Note : Le package org.tweetyproject.logics.pl.* contient toute la logique propositionnelle. Pour d’autres logiques (FOL, Description Logic), les packages changent mais la structure reste similaire.
Construction manuelle de formules
On peut construire des formules de deux facons: 1. Par API Java: en creant des objets Proposition, Negation, etc. 2. Par parsing: en utilisant PlParser pour parser une chaîne
Exemple de construction manuelle:
# --- 2.1.1b Construction manuelle de formules ---if jvm_ready:# Creer des propositions atomiques a = Proposition("a") b = Proposition("b") c = Proposition("c") d = Proposition("d")# Construire des formules complexes f1 = a # a f2 = Negation(b) # !b f3 = Conjunction(a, Negation(c)) # a && !c f4 = Implication(a, b) # a => b# Disjunction necessite une ArrayList list_cd = ArrayList() list_cd.add(c) list_cd.add(d) f5 = Disjunction(list_cd) # c || d# Creer une base de connaissances (Knowledge Base) kb_manual = PlBeliefSet() kb_manual.add(f1) kb_manual.add(f2) kb_manual.add(f3) kb_manual.add(f4) kb_manual.add(f5)print("KB construite manuellement:")print(f" {kb_manual}")print(f"\nFormules individuelles:")print(f" f1 = {f1} (proposition)")print(f" f2 = {f2} (negation)")print(f" f3 = {f3} (conjonction)")print(f" f4 = {f4} (implication)")print(f" f5 = {f5} (disjonction)")else:print("Cellule sautee : JVM Tweety non disponible")
Formules de la KB : 1. a → a est vrai 2. !b → b est faux 3. a && !c → a vrai ET c faux 4. a => b → si a alors b 5. c || d → c ou d (au moins un)
Verification de coherence : - De (1) et (4) : a est vrai, donc a => b implique b vrai - Mais (2) dit !b (b est faux) - CONTRADICTION : Cette KB est insatisfiable !
Point pedagogique : - La construction manuelle permet des formules inconsistantes - C’est voulu pour illustrer la notion de consistance vs inconsistance - Un solveur SAT retournerait UNSAT pour cette KB
Astuce : Toujours verifier la consistance d’une KB avec solver.isSatisfiable(kb) avant de faire des requêtes.
Parsing de formules
Le PlParser permet de parser des formules depuis des chaînes de caractères. La syntaxe utilise les opérateurs: - ! pour la negation - && pour la conjonction - || pour la disjonction - => pour l’implication - <=> pour l’equivalence - ^^ pour le XOR (ou exclusif)
# --- 2.1.1c Parsing de formules ---if jvm_ready:# Parser une base de connaissances depuis une chaine# IMPORTANT: Le parser Tweety est sensible aux espaces de debut de ligne.# Utiliser le format compact avec \n comme separateur de formules. kb_str ="a || b || c\n!a || b\n!b || c" kb_parsed = pl_parser.parseBeliefBase(kb_str)print(f"KB parsee depuis chaine:")print(f" {kb_parsed}")# Parser une formule XOR formula_xor = pl_parser.parseFormula("a ^^ b ^^ c")print(f"\nFormule XOR: {formula_xor}")print(f" Forme DNF: {formula_xor.toDnf()}")# La DNF (Disjunctive Normal Form) montre les 4 interpretations# qui rendent a XOR b XOR c vrai:# - a=T, b=F, c=F# - a=F, b=T, c=F# - a=F, b=F, c=T# - a=T, b=T, c=Telse:print("JVM non demarree - cellule sautee")
KB parsee depuis chaine:
{ !a||b, !b||c, a||b||c }
Formule XOR: a^^b^^c
Forme DNF: (!b&&a&&!c)||(!a&&b&&!c)||(!a&&!b&&c)||(a&&b&&c)
Interpretation du parsing et de la DNF
KB parsee : { !a||b, !b||c, a||b||c }
Cette KB encode une chaîne d’implications : - !a||b = a => b (si a alors b) - !b||c = b => c (si b alors c) - Donc : a => b => c
Interpretation de la DNF : La DNF liste explicitement tous les cas ou la formule est vraie : 1. a=T, b=F, c=F → 1 variable vraie 2. a=F, b=T, c=F → 1 variable vraie 3. a=F, b=F, c=T → 1 variable vraie 4. a=T, b=T, c=T → 3 variables vraies
Pourquoi 4 modèles pour un XOR triple ? - XOR = “nombre impair de variables vraies” - Pour 3 variables : 1 ou 3 vraies → 4 cas possibles - XOR binaire (a ^^ b) n’a que 2 modèles (1 vraie)
Astuce : La méthode toDnf() est utile pour visualiser les modèles d’une formule complexe.
Sémantique : Mondes possibles et satisfaction
Un monde possible (PossibleWorld) est une interpretation qui assigne une valeur de verite a chaque proposition. Une formule est satisfaite par un monde si elle est vraie dans ce monde.
# --- 2.1.1d Semantique : mondes possibles ---if jvm_ready:# Creer un monde possible ou a=True et b=True world1 = PossibleWorld() world1.add(a) # a est vrai world1.add(b) # b est vrai# c et d sont implicitement faux (pas dans le monde)print(f"Monde possible w1 = {world1}")print(f" (Propositions vraies: a, b)")# Verifier si le monde satisfait des formules f_test1 = pl_parser.parseFormula("a && !c") f_test2 = pl_parser.parseFormula("!b") f_test3 = pl_parser.parseFormula("a || c")print(f"\nSatisfaction dans w1:")print(f" w1 |= 'a && !c' ? {world1.satisfies(JObject(f_test1, PlFormula_class))}")print(f" w1 |= '!b' ? {world1.satisfies(JObject(f_test2, PlFormula_class))}")print(f" w1 |= 'a || c' ? {world1.satisfies(JObject(f_test3, PlFormula_class))}")# Enumerer tous les modeles d'une formuleprint(f"\nModeles de 'a ^^ b ^^ c' (XOR):") models =list(formula_xor.getModels())for m in models:print(f" {m}")else:print("Cellule sautee : JVM Tweety non disponible")
Monde possible w1 = [a, b]
(Propositions vraies: a, b)
Satisfaction dans w1:
w1 |= 'a && !c' ? True
w1 |= '!b' ? False
w1 |= 'a || c' ? True
Modeles de 'a ^^ b ^^ c' (XOR):
[a]
[b]
[a, b, c]
[c]
Interpretation des mondes possibles et modèles
Monde possible w1 = [a, b]
Le monde w1 assigne : - a = True - b = True - c = False (implicite) - d = False (implicite)
Tests de satisfaction :
Formule
w1 satisfait ?
Explication
a && !c
True
a=T et c=F → T && T = T
!b
False
b=T → !T = F
a \|\| c
True
a=T → T || F = T
Modèles de a XOR b XOR c :
Les 4 modèles obtenus : 1. [a] → a=T, b=F, c=F → Nombre impair de T 2. [b] → a=F, b=T, c=F → Nombre impair de T 3. [c] → a=F, b=F, c=T → Nombre impair de T 4. [a,b,c] → a=T, b=T, c=T → Nombre impair de T
Sémantique XOR : Vrai si un nombre impair de variables sont vraies.
Distinction importante : - Monde possible : Une interpretation (assignation de verite) - Modèle : Un monde qui satisfait une formule donnee
Conversion DIMACS
Le format DIMACS CNF est le format standard pour les solveurs SAT. Tweety peut convertir une KB en DIMACS pour l’utiliser avec des solveurs externes.
p cnf <nb_variables> <nb_clauses>
<clause1> 0
<clause2> 0
...
Chaque variable est un entier positif, la negation est representee par un entier negatif.
# --- 2.1.1e Conversion DIMACS ---if jvm_ready:# Creer une KB pour la conversion kb_dimacs = PlBeliefSet() kb_dimacs.add(pl_parser.parseFormula("a || b || c")) kb_dimacs.add(pl_parser.parseFormula("!a || b && d")) kb_dimacs.add(pl_parser.parseFormula("a")) kb_dimacs.add(pl_parser.parseFormula("!c"))print(f"KB originale: {kb_dimacs}")print(f"\nConversion en DIMACS CNF:")try: dimacs_lines = DimacsSatSolver.convertToDimacs(kb_dimacs)for line in dimacs_lines:print(f" {str(line).strip()}")print("\nInterpretation:")print(" - 'p cnf 4 5' : 4 variables, 5 clauses")print(" - Variable 1 = a, 2 = b, 3 = c, 4 = d")print(" - '-1' signifie NOT a, '2' signifie b")exceptExceptionas e:print(f" Erreur conversion: {e}")else:print("Cellule sautee : JVM Tweety non disponible")
KB originale: { a||b||c, a, !c, !a||(b&&d) }
Conversion en DIMACS CNF:
p cnf 4 5
1 2 3 0
1 0
-3 0
-1 2 0
-1 4 0
Interpretation:
- 'p cnf 4 5' : 4 variables, 5 clauses
- Variable 1 = a, 2 = b, 3 = c, 4 = d
- '-1' signifie NOT a, '2' signifie b
Interpretation du format DIMACS
Sortie obtenue :
p cnf 4 5
1 2 3 0
1 0
-3 0
-1 2 0
-1 4 0
Decodage ligne par ligne :
Ligne DIMACS
Signification
Formule Tweety
p cnf 4 5
4 variables, 5 clauses
(header)
1 2 3 0
Clause : 1 OR 2 OR 3
a \|\| b \|\| c
1 0
Clause : 1
a
-3 0
Clause : NOT 3
!c
-1 2 0
Clause : NOT 1 OR 2
!a \|\| b
-1 4 0
Clause : NOT 1 OR 4
!a \|\| d
Mapping variables : - Variable 1 → a - Variable 2 → b - Variable 3 → c - Variable 4 → d
Conversion formule → CNF : La formule !a || (b && d) devient 2 clauses : - !a || b → -1 2 0 - !a || d → -1 4 0
Utilite du DIMACS : - Format universel pour tous les solveurs SAT (CaDiCaL, Glucose, MiniSat, Z3) - Permet d’utiliser des solveurs externes sans wrapper Java - Standard de facto depuis les SAT Competitions (1990s)
2.1.2 Raisonnement Simple et Solveurs SAT (SAT4J interne)
Une fois les formules et bases définies, on peut effectuer des raisonnements :
Conséquence Logique (Query) : Déterminer si une formule \(\phi\) est une conséquence logique d’une base \(KB\) (\(KB \models \phi\)).
Satisfiabilité (SAT) : Déterminer si une base \(KB\) admet au moins un modèle (une assignation de vérité qui rend toutes les formules vraies).
Trouver un Modèle (Witness) : Si la base est satisfiable, trouver une assignation de vérité qui la satisfait.
Tweety propose : * SimplePlReasoner : Un raisonneur basique pour la conséquence logique, potentiellement lent. * SatSolver : Une interface pour les solveurs SAT. Sat4jSolver est une implémentation Java intégrée. SatSolver.setDefaultSolver(...) permet de choisir le solveur à utiliser globalement. * isSatisfiable(kb): Vérifie la satisfiabilité. * getWitness(kb): Retourne un PossibleWorld modèle si la KB est satisfiable, sinon None.
# --- 2.1.2 Logique Propositionnelle : Raisonnement et SAT4J ---print("--- 2.1.2 Raisonnement PL et SAT4J ---")ifnot jvm_ready:print("ERREUR: JVM non demarree.")else:try:import jpypefrom jpype.types import JObjectfrom java.util import Collectionfrom org.tweetyproject.logics.pl.syntax import PlBeliefSet, PlFormula, Contradictionfrom org.tweetyproject.logics.pl.parser import PlParserfrom org.tweetyproject.logics.pl.reasoner import SimplePlReasonerfrom org.tweetyproject.logics.pl.sat import SatSolver, Sat4jSolver Collection_class = jpype.JClass("java.util.Collection") pl_parser_sat = PlParser()# === 1. SimplePlReasoner ===print("\n=== 1. SimplePlReasoner (Consequence Logique) ===") kb_str ="a || b || c\n!a || b\n!b || c" kb = pl_parser_sat.parseBeliefBase(kb_str)print(f"KB: {kb}") simple_reasoner = SimplePlReasoner()# Test: KB |= c ?# Explication: (!a||b) signifie a=>b, et (!b||c) signifie b=>c# Donc a => b => c, et avec (a||b||c), on deduit c query_c = pl_parser_sat.parseFormula("c") result_c = simple_reasoner.query(kb, query_c)print(f"\nKB |= c ? {result_c}")print(" Explication: a=>b et b=>c impliquent que c est toujours vrai")# Test: KB est-elle consistante ? query_false = Contradiction() result_cons = simple_reasoner.query(kb, query_false)print(f"\nKB |= contradiction ? {result_cons}")print(" False => la KB est consistante")# === 2. SAT4J Solver ===print("\n=== 2. SAT4J Solver (Satisfiabilite) ===") SatSolver.setDefaultSolver(Sat4jSolver()) solver = SatSolver.getDefaultSolver()# Exemple SAT kb_sat = PlBeliefSet() kb_sat.add(pl_parser_sat.parseFormula("a || b")) kb_sat.add(pl_parser_sat.parseFormula("!a || c")) kb_sat.add(pl_parser_sat.parseFormula("b || !c"))print(f"\nKB SAT: {kb_sat}")print(f" Satisfiable? {solver.isSatisfiable(kb_sat)}")# Exemple UNSAT kb_unsat = PlBeliefSet() kb_unsat.add(pl_parser_sat.parseFormula("a")) kb_unsat.add(pl_parser_sat.parseFormula("!a"))print(f"\nKB UNSAT (contradiction): {kb_unsat}")print(f" Satisfiable? {solver.isSatisfiable(kb_unsat)}")# === 3. Enumeration des Modeles ===print("\n=== 3. Enumeration des Modeles ===")# Pour une formule simple, on peut enumerer tous les modeles formula_simple = pl_parser_sat.parseFormula("a ^^ b") # XOR models =list(formula_simple.getModels())print(f"Formule: a XOR b")print(f"Nombre de modeles: {len(models)}")for m in models:print(f" Modele: {m}")exceptExceptionas e:print(f"Erreur: {e}")import traceback traceback.print_exc()
--- 2.1.2 Raisonnement PL et SAT4J ---
=== 1. SimplePlReasoner (Consequence Logique) ===
KB: { !a||b, !b||c, a||b||c }
KB |= c ? True
Explication: a=>b et b=>c impliquent que c est toujours vrai
KB |= contradiction ? False
False => la KB est consistante
=== 2. SAT4J Solver (Satisfiabilite) ===
KB SAT: { b||!c, a||b, !a||c }
Satisfiable? True
KB UNSAT (contradiction): { !a, a }
Satisfiable? False
=== 3. Enumeration des Modeles ===
Formule: a XOR b
Nombre de modeles: 2
Modele: [a]
Modele: [b]
Interpretation des résultats SAT4J
Exemple 1 : SimplePlReasoner (Consequence Logique)
Requête : KB |= c ?
Résultat : True
Explication detaillee :
KB = { a||b||c, !a||b, !b||c }
Reformulation en implications :
!a||b ≡ a => b
!b||c ≡ b => c
Donc: a => b => c
Avec a||b||c, on deduit que c est toujours vrai
Verification de consistance : - KB |= contradiction ? → False ✓ - La KB est consistante (au moins un modèle existe)
Exemple 2 : SAT4J Solver
KB
Résultat
Explication
{ a\|\|b, !a\|\|c, b\|\|!c }
SAT
Modèle possible : a=True, b=True, c=True
{ a, !a }
UNSAT
Contradiction directe
Exemple 3 : Enumeration des modèles
Formule : a XOR b - Modèle 1 : [a] (a=True, b=False) - Modèle 2 : [b] (a=False, b=True)
Remarque : XOR a exactement 2 modèles, comme attendu par la table de verite.
SAT4J vs solveurs modernes : SAT4J est portable (Java pur) mais ~10-20x plus lent que CaDiCaL/Glucose sur des problemes >1000 variables.
2.1.3 Solveurs SAT Modernes (pySAT / CaDiCaL)
Sat4j est un excellent solveur SAT en Java pur, mais pour des problèmes industriels complexes, les solveurs natifs modernes offrent des performances nettement supérieures.
Hiérarchie des Solveurs SAT (2024):
Solveur
Origine
Performance
Caractéristique
CaDiCaL 1.9.5
A. Biere (TU Wien)
⭐⭐⭐⭐⭐
Champion SAT Competition, base de Kissat
CryptoMiniSat 5
M. Soos
⭐⭐⭐⭐
Spécialisé XOR (cryptographie)
Glucose 4.2
G. Audemard
⭐⭐⭐⭐
Apprentissage de lemmes agressif
MapleChrono
V. Liang
⭐⭐⭐⭐
Backtracking chronologique
Lingeling
A. Biere
⭐⭐⭐
Prédécesseur stable de CaDiCaL
Sat4j
D. Le Berre
⭐⭐⭐
Java pur, portable
Pourquoi pySAT ?
# Installation: pip install python-satfrom pysat.solvers import Solverfrom pysat.formula import CNF# pySAT wraps native C/C++ solvers with Python interface# - Same solver quality as standalone executables# - Convenient Python API (no subprocess, no DIMACS files)# - Incremental solving, assumptions, UNSAT cores
Comparaison typique (problème 10,000 variables): - Sat4j: ~10 secondes - CaDiCaL: ~0.5 secondes - Ratio: 20x plus rapide
# --- 2.1.3a Solveurs SAT Modernes: Comparaison sur Problemes Varies ---print("--- 2.1.3a Solveurs SAT Modernes (pySAT / CaDiCaL) ---")pysat_available =Falsetry:from pysat.solvers import Solver pysat_available =Trueprint("pySAT installe - acces aux solveurs modernes!")exceptImportError:print("pySAT non installe. Installez avec: pip install python-sat")if pysat_available:import timeimport random random.seed(42)def generate_pigeonhole(n_pigeons, n_holes):"""Pigeonhole: n pigeons dans n-1 trous (UNSAT classique)""" clauses = []for i inrange(1, n_pigeons +1): clauses.append([(i-1)*n_holes + j for j inrange(1, n_holes +1)])for j inrange(1, n_holes +1):for i1 inrange(1, n_pigeons +1):for i2 inrange(i1 +1, n_pigeons +1): clauses.append([-(i1-1)*n_holes - j, -(i2-1)*n_holes - j])return clausesdef generate_latin_square(n, seed=42):"""Latin Square: chaque valeur une fois par ligne/colonne""" random.seed(seed) clauses = []def var(r, c, v): return r * n * n + c * n + v +1for r inrange(n):for c inrange(n): clauses.append([var(r, c, v) for v inrange(n)])for r inrange(n):for c inrange(n):for v1 inrange(n):for v2 inrange(v1 +1, n): clauses.append([-var(r, c, v1), -var(r, c, v2)])for r inrange(n):for v inrange(n):for c1 inrange(n):for c2 inrange(c1 +1, n): clauses.append([-var(r, c1, v), -var(r, c2, v)])for c inrange(n):for v inrange(n):for r1 inrange(n):for r2 inrange(r1 +1, n): clauses.append([-var(r1, c, v), -var(r2, c, v)])return clausesdef generate_queens(n):"""N-Queens: placer n reines sans attaques""" clauses = []def var(r, c): return (r-1)*n + cfor r inrange(1, n+1): clauses.append([var(r, c) for c inrange(1, n+1)])for r inrange(1, n+1):for c1 inrange(1, n+1):for c2 inrange(c1+1, n+1): clauses.append([-var(r, c1), -var(r, c2)])for c inrange(1, n+1):for r1 inrange(1, n+1):for r2 inrange(r1+1, n+1): clauses.append([-var(r1, c), -var(r2, c)])for r inrange(1, n+1):for c inrange(1, n+1):for d inrange(1, n):if r+d <= n and c+d <= n: clauses.append([-var(r, c), -var(r+d, c+d)])if r+d <= n and c-d >=1: clauses.append([-var(r, c), -var(r+d, c-d)])return clausesdef benchmark(name, clauses, solvers=['cadical195', 'glucose42', 'minisat22']): n_vars =max(abs(lit) for clause in clauses for lit in clause) if clauses else0print(f"\n--- {name} ---")print(f" {n_vars} vars, {len(clauses)} clauses") results = {}for s in solvers:try:with Solver(name=s, bootstrap_with=clauses) as solver: t0 = time.perf_counter() res = solver.solve() ms = (time.perf_counter() - t0) *1000 results[s] = (res, ms)print(f" {s:12s}: {'SAT'if res else'UNSAT':5s}{ms:8.2f}ms")except:print(f" {s:12s}: N/A")return results# Warmup pour eviter le biais cold-startprint("\n[Warmup des solveurs...]") warmup_cnf = [[1, 2], [-1, 2], [1, -2]]for s in ['cadical195', 'glucose42', 'minisat22']:with Solver(name=s, bootstrap_with=warmup_cnf) as solver: solver.solve()print("[Warmup termine]") all_results = {}# Test 1: Pigeonhole 9->8 (UNSAT) - CaDiCaL excelle sur UNSAT difficiles all_results['T1'] = benchmark("Test 1: Pigeonhole 9->8 (UNSAT crafted)", generate_pigeonhole(9, 8))# Test 2: Latin Square 13x13 (SAT) - Glucose excelle sur cette taille all_results['T2'] = benchmark("Test 2: Latin Square 13x13 (SAT combinatoire)", generate_latin_square(13))# Test 3: N-Queens 35 (SAT geometrique) - MiniSat excelle all_results['T3'] = benchmark("Test 3: 35-Queens (SAT geometrique)", generate_queens(35))# Comptage des victoiresprint("\n"+"="*50) wins = {s: 0for s in ['cadical195', 'glucose42', 'minisat22']}for res in all_results.values():if res: best =min(res, key=lambda s: res[s][1]) wins[best] +=1print(f"Victoires: CaDiCaL={wins['cadical195']}, Glucose={wins['glucose42']}, MiniSat={wins['minisat22']}")print("\nConclusion (narrateur data-driven, issu des victoires calculees):") _label = {'cadical195': 'CaDiCaL', 'glucose42': 'Glucose', 'minisat22': 'MiniSat'} _test_label = {'T1': 'Pigeonhole (UNSAT)', 'T2': 'Latin Square 13x13', 'T3': '35-Queens'}for _tk in ['T1', 'T2', 'T3']: _res = all_results.get(_tk)if _res: _best =min(_res, key=lambda s: _res[s][1])print(f"- {_test_label[_tk]}: {_label[_best]} le plus rapide ({_res[_best][1]:.2f}ms)")print(f"=> Victoires: CaDiCaL={wins['cadical195']}, Glucose={wins['glucose42']}, MiniSat={wins['minisat22']}")else:print("Pour utiliser les solveurs modernes: pip install python-sat")
--- 2.1.3a Solveurs SAT Modernes (pySAT / CaDiCaL) ---
pySAT installe - acces aux solveurs modernes!
[Warmup des solveurs...]
[Warmup termine]
--- Test 1: Pigeonhole 9->8 (UNSAT crafted) ---
72 vars, 297 clauses
cadical195 : UNSAT 255.56ms
glucose42 : UNSAT 2237.76ms
minisat22 : UNSAT 321.71ms
--- Test 2: Latin Square 13x13 (SAT combinatoire) ---
2197 vars, 39715 clauses
cadical195 : SAT 1.56ms
glucose42 : SAT 1.39ms
minisat22 : SAT 1.57ms
--- Test 3: 35-Queens (SAT geometrique) ---
1225 vars, 69055 clauses
cadical195 : SAT 27.34ms
glucose42 : SAT 2.82ms
minisat22 : SAT 1.53ms
==================================================
Victoires: CaDiCaL=1, Glucose=1, MiniSat=1
Conclusion (narrateur data-driven, issu des victoires calculees):
- Pigeonhole (UNSAT): CaDiCaL le plus rapide (255.56ms)
- Latin Square 13x13: Glucose le plus rapide (1.39ms)
- 35-Queens: MiniSat le plus rapide (1.53ms)
=> Victoires: CaDiCaL=1, Glucose=1, MiniSat=1
Interpretation des benchmarks pySAT
Résultats obtenus (sortie de la cellule précédente) :
Test
Vainqueur
Remarque
Pigeonhole 9→8 (UNSAT)
CaDiCaL
UNSAT crafted difficile
Latin Square 13×13 (SAT)
Glucose
Apprentissage de lemmes
35-Queens (SAT)
MiniSat
Géométrie bien exploitée
Les temps absolus mesurés par chaque solveur sont le résultat live de la cellule de benchmark ci-dessus : ils varient d’une machine à l’autre (CPU, version Java/pySAT, charge), règle #9434 — la prose ne les fige pas. Ce qui est stable cross-machine, c’est le vainqueur de chaque instance et l’ordre de grandeur du rapport entre solveurs :
Instance
Vainqueur
Ordre de grandeur
Diagnostic
Pigeonhole 9→8 (UNSAT, 72 var / 297 cl)
CaDiCaL
CaDiCaL ~6× plus rapide que Glucose
UNSAT crafted : CaDiCaL domine
Latin Square 13×13 (SAT, 2197 var / 39715 cl)
Glucose
3 solveurs quasi ex-aequo (quelques ms)
SAT combinatoire petit : lemmes prime
35-Queens (SAT, 1225 var / 69055 cl)
MiniSat
MiniSat ~30× plus rapide que CaDiCaL
SAT géométrique : heuristique spatiale
Victoires : CaDiCaL = 1, Glucose = 1, MiniSat = 1 (comptage automatique dans le code).
Analyse :
Pigeonhole (UNSAT crafted) :
CaDiCaL domine nettement (~6× plus rapide que Glucose)
Glucose peine sur cet UNSAT difficile (plusieurs secondes)
MiniSat se place en intermédiaire
Latin Square (SAT combinatoire) :
Les trois solveurs sont très proches (quelques ms) : instance petite, marge étroite
Glucose légèrement en tête grâce à son apprentissage de lemmes agressif
Structure régulière favorable aux heuristiques modernes
N-Queens (SAT géométrique) :
MiniSat excelle ; CaDiCaL nettement en retrait (~30× plus lent)
Les contraintes géométriques jouent en faveur de l’heuristique MiniSat
Glucose également performant
Conclusion : - Aucun solveur universel : chaque problème a son champion - CaDiCaL : Robuste sur UNSAT difficiles et formules industrielles - Glucose : Excellent sur problèmes combinatoires avec structure - MiniSat : Rapide sur géométrie et contraintes spatiales
Honnêteté sur la mesure. Les temps produits par la cellule de benchmark ci-dessus sont un instantané d’une exécution sur une machine donnée — ils ne sont pas figés dans cette prose (règle #9434) : seuls les vainqueurs et les ordres de grandeur structurels sont reportés ici, car ce sont les seuls stables d’une machine à l’autre. À l’échelle de la milliseconde (Latin Square), le classement d’un test peut même basculer d’une exécution à l’autre — par exemple une exécution précédente donnait MiniSat vainqueur sur Latin Square (Glucose à 0 victoire). C’est précisément pour cela que la cellule de code calcule le vainqueur de chaque test (min(..., key=...)) et déduit sa conclusion du comptage de victoires plutôt que d’afficher des spécialités figées : la narration suit toujours les résultats réels, sans dérive.
Enumeration de solutions et integration Tweety
Au-dela de trouver UNE solution, pySAT permet d’enumerer toutes les solutions d’un problème SAT en ajoutant des clauses de blocage.
L’integration avec Tweety est simple: 1. Créer une KB avec PlParser et PlBeliefSet 2. Convertir en DIMACS avec DimacsSatSolver.convertToDimacs() 3. Parser le DIMACS pour pySAT 4. Resoudre avec CaDiCaL ou autre solveur moderne
# --- 2.1.3b Enumeration et Integration Tweety ---print("--- 2.1.3b Enumeration de solutions et Integration Tweety ---")ifnot pysat_available:print("pySAT non disponible - cellule sautee.")else:# === Exemple 3: Enumeration de solutions ===print("\n--- Exemple 3: Enumeration des solutions ---")# Formule: a XOR b (exactement un des deux vrai) xor_clauses = [[1, 2], [-1, -2]] # (a OR b) AND (NOT a OR NOT b)print(f"Clauses a XOR b: {xor_clauses}") solutions = []with Solver(name='cadical195', bootstrap_with=xor_clauses) as solver:while solver.solve(): model = solver.get_model() solutions.append(model) solver.add_clause([-lit for lit in model]) # Clause de blocageprint(f" Nombre de solutions: {len(solutions)}")for i, sol inenumerate(solutions):print(f" Solution {i+1}: a={sol[0]>0}, b={sol[1]>0}")# === Exemple 4: Integration avec Tweety ===if jvm_ready:print("\n--- Exemple 4: Integration Tweety -> pySAT ---")try:from org.tweetyproject.logics.pl.sat import DimacsSatSolverfrom org.tweetyproject.logics.pl.syntax import PlBeliefSetfrom org.tweetyproject.logics.pl.parser import PlParser# Creer une KB Tweety parser_sat = PlParser() kb_sat = PlBeliefSet() kb_sat.add(parser_sat.parseFormula("a || b || c")) kb_sat.add(parser_sat.parseFormula("!a || !b")) kb_sat.add(parser_sat.parseFormula("!b || !c")) kb_sat.add(parser_sat.parseFormula("a || c"))print(f" KB Tweety: {kb_sat}")# Convertir en DIMACS dimacs_lines = [str(line) for line in DimacsSatSolver.convertToDimacs(kb_sat)]print(" Format DIMACS:")for line in dimacs_lines:print(f" {line.strip()}")# Parser DIMACS pour pySAT cnf_pysat = []for line in dimacs_lines: line = line.strip()if line andnot line.startswith(('c', 'p')): lits = [int(x) for x in line.split() if x !='0']if lits: cnf_pysat.append(lits)# Resoudre avec CaDiCaLwith Solver(name='cadical195', bootstrap_with=cnf_pysat) as solver:if solver.solve(): model = solver.get_model()print(f"\n Resultat CaDiCaL: SAT")print(f" Modele: {model[:4]}") # 4 variableselse:print(f"\n Resultat CaDiCaL: UNSAT")exceptExceptionas e:print(f" Erreur integration: {e}")print("\n==> pySAT est un excellent complement a Tweety pour le SAT.")
--- 2.1.3b Enumeration de solutions et Integration Tweety ---
--- Exemple 3: Enumeration des solutions ---
Clauses a XOR b: [[1, 2], [-1, -2]]
Nombre de solutions: 2
Solution 1: a=True, b=False
Solution 2: a=False, b=True
--- Exemple 4: Integration Tweety -> pySAT ---
KB Tweety: { !a||!b, !b||!c, a||c, a||b||c }
Format DIMACS:
p cnf 3 4
-1 -2 0
-2 -3 0
1 3 0
1 2 3 0
Resultat CaDiCaL: SAT
Modele: [1, -2, 3]
==> pySAT est un excellent complement a Tweety pour le SAT.
Interpretation : Enumeration et integration Tweety -> pySAT
Exemple 3 : Enumeration de solutions
Résultat : 2 solutions trouvees pour a XOR b - Solution 1 : a=True, b=False - Solution 2 : a=False, b=True
Technique de blocage :
while solver.solve(): model = solver.get_model() solutions.append(model) solver.add_clause([-lit for lit in model]) # Bloque cette solution
La clause [-lit for lit in model] interdit exactement ce modèle.
Exemple 4 : Integration Tweety -> pySAT
Workflow complet : 1. Tweety : Créer KB avec PlParser et PlBeliefSet 2. Conversion : DimacsSatSolver.convertToDimacs(kb) → format DIMACS 3. Parsing : Extraire les clauses du DIMACS pour pySAT 4. Resolution : CaDiCaL trouve un modèle : [1, -2, 3] = a=True, b=False, c=True
Avantages de cette approche : - Tweety pour la modelisation (syntaxe intuitive) - pySAT pour la performance (solveurs C++ natifs) - Meilleur des deux mondes : expressivite + vitesse
Use case reel : Verification formelle, planning, configuration de produits
Exercice : Coloration de graphe avec SAT
Contexte
Un graphe non orienté à 4 sommets (A, B, C, D) a les arêtes suivantes : A-B, A-C, B-C, C-D. On souhaite colorier chaque sommet avec exactement une des 3 couleurs (Rouge, Vert, Bleu) de sorte que deux sommets adjacents n’aient jamais la même couleur.
Objectifs
Modéliser ce problème en logique propositionnelle avec 12 variables X_couleur
Construire la KB Tweety contenant les contraintes (au moins une couleur, au plus une couleur, exclusion arêtes)
Résoudre avec Sat4jSolver et afficher la coloration trouvée
Vérifier qu’une 2-coloration (2 couleurs seulement) est impossible sur ce graphe
“Au plus une couleur” : !(a_r && a_v) && !(a_r && a_b) && !(a_v && a_b)
“Pas même couleur sur arête A-B” : !(a_r && b_r) && !(a_v && b_v) && !(a_b && b_b)
# --- Exercice : Coloration de graphe (4 sommets, 3 couleurs) ---# TODO etudiant : Construisez la KB avec les 12 variables et les contraintes# Etape 1 : Creer les 12 propositions (a_r, a_v, a_b, b_r, ...)# Etape 2 : Pour chaque sommet, ajouter "au moins une couleur" et "au plus une couleur"# Etape 3 : Pour chaque arete, ajouter les contraintes d'exclusion de couleur# Etape 4 : Resoudre avec Sat4jSolver et afficher la coloration# Etape 5 : Retirer une couleur et verifier que le probleme devient UNSATprint("Exercice a completer")
Exercice a completer
2.2 Logique du Premier Ordre (FOL)
La logique du premier ordre (First-Order Logic, FOL) étend la logique propositionnelle avec des prédicats, constantes, variables et quantificateurs.
Signature = Sorts + Constantes + Prédicats + Fonctions
Sorts (Types): Humain, Animal, ...
Constantes: jean, marie, fido (instances spécifiques)
Variables: X, Y, Z (placeholders)
Prédicats: Aime/2, Mortel/1, EstParentDe/2
Fonctions: pere/1, mere/1 (retournent un terme)
Quantificateurs:
- forall X: ... "Pour tout X, ..."
- exists X: ... "Il existe X tel que ..."
Exemple classique - Syllogisme:
forall X: (Humain(X) => Mortel(X)) // Tous les humains sont mortels
Humain(socrate) // Socrate est humain
----------------------------------------
?- Mortel(socrate) // Donc: Socrate est mortel
Classes Tweety principales: * FolSignature: Définit les Sorts, Constants et Predicates * FolFormula: Formules incluant atomes, connecteurs et quantificateurs * FolParser: Parse les formules (nécessite une signature) * FolReasoner: Interface pour les prouveurs (EProver, SPASS, SimpleFolReasoner)
Limitation connue:SimpleFolReasoner peut causer des problèmes de heap space sur des requêtes complexes avec égalité. Pour un raisonnement FOL robuste, utilisez EProver.
Transition : De la Logique Propositionnelle a la Logique du Premier Ordre
Ce que nous avons vu en PL : - Propositions atomiques (a, b, rain, umbrella) - Connecteurs logiques (&&, ||, !, =>) - Mondes possibles et satisfiabilite - Solveurs SAT modernes (CaDiCaL, Glucose)
Limites de PL : - Impossible de representer “Tous les humains sont mortels” - Chaque instance necessite une proposition separee : mortel_socrate, mortel_platon, … - Pas de generalisation ni de quantification
Ce que FOL apporte : - Predicats : Relations entre objets (Aime(jean, marie)) - Quantificateurs : Generalisation (forall X: Humain(X) => Mortel(X)) - Variables : Placeholders pour les objets - Fonctions : Termes composes (pere(jean), age(X) + 1)
Exemple concret :
Representation
Logique Propositionnelle
Logique du Premier Ordre
“Socrate est mortel”
mortel_socrate
Mortel(socrate)
“Tous les humains sont mortels”
mortel_socrate && mortel_platon && ... (infini!)
forall X: (Humain(X) => Mortel(X))
“Il existe quelqu’un qui aime Marie”
Impossible sans enumeration
exists X: Aime(X, marie)
Prix a payer : - PL est decidable (SAT est NP-complet, mais resolvable) - FOL est semi-decidable (peut boucler indefiniment sur certaines requêtes) - Les solveurs FOL (EProver, SPASS) utilisent des heuristiques pour terminer en pratique
# --- 2.2.1 Imports et verification JVM pour FOL ---print("--- 2.2.1 Imports FOL ---")ifnot jvm_ready:print("ERREUR: JVM non demarree. Impossible de continuer cet exemple.") fol_imports_ok =Falseelse:print("JVM prete. Import des classes FOL...") fol_imports_ok =Falsetry:# Imports Java necessairesimport jpypefrom jpype.types import*from java.util import ArrayList, Collectionimport pathlib# Imports FOL Tweetyfrom org.tweetyproject.logics.fol.syntax import FolFormula, FolSignature, FolBeliefSet, FolAtomfrom org.tweetyproject.logics.commons.syntax import Sort, Constant, Predicate, Variablefrom org.tweetyproject.logics.fol.parser import FolParserfrom org.tweetyproject.logics.fol.reasoner import FolReasoner, SimpleFolReasoner, EFOLReasoner# Verification des utilitairesif'get_tool_path'notinglobals() or'EXTERNAL_TOOLS'notinglobals():raiseNameError("La fonction 'get_tool_path' ou 'EXTERNAL_TOOLS' n'est pas definie.")# Classes Java pour casting Collection_class = jpype.JClass("java.util.Collection") FolFormula_class = jpype.JClass("org.tweetyproject.logics.fol.syntax.FolFormula")print("Imports FOL/Commons reussis.") fol_imports_ok =TrueexceptImportErroras e:print(f"Erreur d'import pour FOL : {e}")except jpype.JException as e_java:print(f"Erreur Java generale FOL: {e_java.message()}")exceptExceptionas e_gen:print(f"Erreur Python inattendue FOL: {e_gen}")
Succes : Tous les imports FOL ont ete charges correctement.
Classes importees : - FolFormula, FolSignature, FolBeliefSet : Structures de base FOL - Sort, Constant, Predicate, Variable : Éléments de signature - FolParser : Parsing de formules textuelles - FolReasoner, SimpleFolReasoner, EFOLReasoner : Moteurs d’inference
Différence avec PL : - PL utilise PlFormula, PlSignature, PlBeliefSet - FOL ajoute le package commons.syntax pour les éléments partages (Sort, Constant, etc.) - La hiérarchie Tweety separe la syntaxe (formules) de la sémantique (raisonnement)
Astuce : Les classes *Reasoner sont des facades pour les solveurs externes (EProver, SPASS). Le pattern reste le même : reasoner.query(kb, formula) retourne True/False.
Creation de la Signature FOL
Une signature définit le vocabulaire de notre domaine : - Sorts : Les types d’objets (Person, City) - Constantes : Les individus (alice, bob, paris, london) - Predicats : Les relations entre objets (livesIn/2, mortal/1, isHappy/1)
Signature:
Sorts: Person, City
Constantes: alice, bob : Person
paris, london : City
Predicats: livesIn(Person, City)
mortal(Person)
isHappy(Person)
# --- 2.2.2 Creation de la Signature FOL ---if fol_imports_ok:# Creer une signature avec predicats d'egalite integres sig_fol = FolSignature(True)# Definir les Sorts (types) sort_person = Sort("Person") sort_city = Sort("City") sig_fol.add(sort_person) sig_fol.add(sort_city)# Definir les Constantes alice = Constant("alice", sort_person) bob = Constant("bob", sort_person) paris = Constant("paris", sort_city) london = Constant("london", sort_city) sig_fol.add(alice) sig_fol.add(bob) sig_fol.add(paris) sig_fol.add(london)# Definir les Predicats livesIn_arity = ArrayList() livesIn_arity.add(sort_person) livesIn_arity.add(sort_city) livesIn = Predicate("livesIn", livesIn_arity) mortal_arity = ArrayList() mortal_arity.add(sort_person) mortal = Predicate("mortal", mortal_arity) isHappy_arity = ArrayList() isHappy_arity.add(sort_person) isHappy = Predicate("isHappy", isHappy_arity) sig_fol.add(livesIn) sig_fol.add(mortal) sig_fol.add(isHappy)print("Signature FOL creee:")print(sig_fol)else:print("Imports FOL non disponibles - signature non creee.")
Signature FOL creee:
[_Any = {}, City = {london, paris}, Person = {alice, bob}], [isHappy(Person), ==(_Any,_Any), /==(_Any,_Any), livesIn(Person,City), mortal(Person)], []
Interpretation de la signature créée
Sortie obtenue : La signature contient 3 sorts, 5 predicats, et 0 fonctions.
Pourquoi les types (Sorts) ? - Evitent les formules mal typees : livesIn(paris, alice) serait rejetee - Permettent l’optimisation des prouveurs (reduction de l’espace de recherche) - Utiles pour le typage statique des théories formelles
Note : Le type _Any est ajoute automatiquement pour les predicats d’egalite polymorphes.
Parsing de formules FOL
Le FolParser permet de parser des formules textuelles vers des objets FolFormula.
Syntaxe Tweety FOL :
Atome: livesIn(alice, paris)
Egalite: paris /== london (différente de)
paris == london (egal a)
Connecteurs: f1 && f2, f1 || f2, !f1, f1 => f2
Quantif.: forall X: (Humain(X) => Mortel(X))
exists X: Aime(X, marie)
Limitation Tweety 1.28: Le parsing de quantificateurs complexes peut echouer. Nous utilisons des faits atomiques simples pour cet exemple.
# --- 2.2.3 Parsing et Base de Croyances FOL ---if fol_imports_ok:# Creer le parser avec notre signature parser_fol = FolParser() parser_fol.setSignature(sig_fol)# Creer la base de croyances kb_fol = FolBeliefSet()# Formules a parser (limitees aux faits atomiques pour eviter les bugs) formulas_to_test = ["livesIn(alice, paris)", # Alice vit a Paris"livesIn(bob, london)", # Bob vit a Londres"paris /== london", # Paris et Londres sont differents"isHappy(alice)"# Alice est heureuse ]print("Parsing des formules FOL dans la KB...") parsing_ok =Truefor f_str in formulas_to_test:try: formula_obj = parser_fol.parseFormula(f_str) kb_fol.add(JObject(formula_obj, FolFormula_class))print(f" [OK] {f_str}")except jpype.JException as e_parse:print(f" [ERREUR] {f_str}: {e_parse.message()}") parsing_ok =FalseexceptExceptionas e_gen:print(f" [ERREUR] {f_str}: {e_gen}") parsing_ok =Falseprint(f"\nKB FOL contient {kb_fol.size()} formules.")else:print("Imports FOL non disponibles - parsing ignore.")
Parsing des formules FOL dans la KB...
[OK] livesIn(alice, paris)
[OK] livesIn(bob, london)
[OK] paris /== london
[OK] isHappy(alice)
KB FOL contient 4 formules.
Interpretation du parsing FOL
Succes : Les 4 formules ont ete parsees correctement dans la KB.
Formules parsees : 1. livesIn(alice, paris) - Predicat binaire avec deux constantes typees 2. livesIn(bob, london) - Même predicat, instances différentes 3. paris /== london - Negation d’egalite (les deux villes sont distinctes) 4. isHappy(alice) - Predicat unaire
Différence avec PL : - En PL, alice_a_paris serait une proposition atomique (indivisible) - En FOL, livesIn(alice, paris) est un atome structure : - livesIn : predicat (relation) - alice, paris : termes (constantes typees) - Permet de raisonner sur les relations entre entites
Limitation du parsing simple : - Les formules ci-dessus sont des faits atomiques (ground atoms) - Pour des quantificateurs comme forall X: livesIn(X, paris), le parsing Tweety 1.28 peut echouer - Solution : construire manuellement avec ForallQuantifiedFormula ou utiliser EProver en mode TPTP
Raisonnement FOL : Choix du Raisonneur
Tweety propose plusieurs raisonneurs FOL :
Raisonneur
Description
Performance
SimpleFolReasoner
Enumeration exhaustive
Lent, heap space sur egalite
EFOLReasoner
Utilise EProver externe
Rapide, robuste
SPASSReasoner
Utilise SPASS externe
Rapide, moins portable
Recommandation: Utilisez EProver pour un raisonnement FOL robuste. SimpleFolReasoner est acceptable pour des KB très petites sans requêtes d’egalite.
# --- 2.2.4 Configuration du Raisonneur et Requetes ---if fol_imports_ok and kb_fol.size() >0: fol_reasoner =None# Tenter EProver si configure eprover_path_str = get_tool_path('EPROVER')if eprover_path_str:print(f"Configuration EProver: {eprover_path_str}")try: fol_reasoner = EFOLReasoner(JString(eprover_path_str)) FolReasoner.setDefaultReasoner(fol_reasoner)print(" EProver configure comme raisonneur par defaut.")exceptExceptionas e_eprover:print(f" Erreur EProver: {e_eprover}") fol_reasoner =None# Fallback vers SimpleFolReasonerifnot fol_reasoner:print("Utilisation de SimpleFolReasoner (fallback).") fol_reasoner = SimpleFolReasoner() FolReasoner.setDefaultReasoner(fol_reasoner) current_reasoner = FolReasoner.getDefaultReasoner()print(f"\nRaisonneur actif: {current_reasoner.getClass().getSimpleName()}")else:print("KB vide ou imports manquants - raisonnement ignore.")
Configuration EProver: <repo>MyIA.AI.Notebooks\SymbolicAI\ext_tools\EProver\eprover.exe
EProver configure comme raisonneur par defaut.
Raisonneur actif: EFOLReasoner
Interpretation de la configuration du raisonneur
Résultat : EProver a ete configure avec succes comme raisonneur par defaut.
Comparaison des raisonneurs :
Critere
SimpleFolReasoner
EProver
SPASS
Installation
Integre Tweety
Binaire externe
Binaire externe
Performance
Très lente
Excellente
Excellente
Robustesse
Heap space frequents
Robuste
Robuste
Portabilite
100% (Java)
Multi-OS
Windows + Linux
Cas d’usage
Tests basiques
Production
Logique modale
Astuce : EProver est le choix recommande pour tout raisonnement FOL au-dela des tests triviaux.
Exécution de Requêtes FOL
Testons l’inference logique sur notre base de connaissances : - livesIn(alice, paris) - Presente dans KB → True - livesIn(bob, paris) - Non deductible → False - isHappy(alice) - Presente dans KB → True - isHappy(bob) - Non deductible → False
Note: La requête paris == london est retiree car elle provoque un heap space avec SimpleFolReasoner (enumeration exhaustive de tous les mondes possibles).
# --- 2.2.5 Execution des Requetes ---if fol_imports_ok and kb_fol.size() >0:# Requetes simples (sans egalite de constantes) queries_fol_str = ["livesIn(alice, paris)", # Devrait etre True"livesIn(bob, paris)", # Devrait etre False"isHappy(alice)", # Devrait etre True"isHappy(bob)", # Devrait etre False/Unknown ]print("Resultats des requetes:")for q_str in queries_fol_str:try: query_formula = parser_fol.parseFormula(q_str) result = current_reasoner.query(kb_fol, JObject(query_formula, FolFormula_class)) result_str ='Unknown'if result isNoneelsestr(result)print(f" {q_str} → {result_str}")except jpype.JException as e_query_java:print(f" {q_str} → ERREUR JAVA: {e_query_java.message()}")exceptExceptionas e_query_py:print(f" {q_str} → ERREUR: {e_query_py}")print("\n[Note] Requete 'paris == london' retiree - voir section 2.2.1 pour EProver.")else:print("Raisonnement FOL ignore (KB vide ou imports manquants).")
Aucune information sur le bonheur de Bob (hypothese du monde clos)
Hypothese du monde clos (Closed World Assumption) : - Ce qui n’est pas prouvable est considere FAUX - Oppose a l’hypothese du monde ouvert (OWA) utilisee en Semantic Web - Typique des bases de données et systèmes experts
Différence FOL vs PL : - En PL, on manipule des propositions atomiques (a, b, c) - En FOL, on manipule des relations entre objets (livesIn(alice, paris)) - La FOL permet de factoriser avec des quantificateurs : forall X: (Humain(X) => Mortel(X))
Exercice : Modélisation FOL d’un domaine géographique
Contexte
Vous disposez d’une base de connaissances géographique simplifiée : - Paris et Lyon sont des villes françaises. Londres est une ville anglaise. - capitalOf(paris, france) et capitalOf(london, uk) sont des faits. - locatedIn(paris, europe) et locatedIn(lyon, europe) sont des faits.
Objectifs
Créer une signature FOL avec les sorts City, Country, Region et les prédicats capitalOf/2, locatedIn/2, sameCountry/2
Peupler la KB avec les faits ci-dessus
Ajouter manuellement des règles : si deux villes sont dans le même pays (sameCountry), elles sont dans la même région
Suivez le même pattern que la section 2.2 : créer FolSignature, ajouter sorts, constantes, prédicats
Utilisez FolParser avec la signature pour parser les faits
Pour les règles avec quantificateurs, utilisez EProver (SimpleFolReasoner ne les gère pas bien)
# --- Exercice : Modelisation FOL d'un domaine geographique ---# TODO etudiant : Creez la signature, la KB et executez les requetes# Etape 1 : Definir les sorts (City, Country, Region) et les constantes (paris, lyon, london, france, uk, europe)# Etape 2 : Definir les predicats (capitalOf/2, locatedIn/2, sameCountry/2)# Etape 3 : Peupler la KB avec les faits# Etape 4 : Executer les requetes avec EProver ou SimpleFolReasonerprint("Exercice a completer")
Exercice a completer
2.2.6 Test EProver (Optionnel)
EProver est un prouveur automatique de theoremes pour la logique du premier ordre. Il est beaucoup plus efficace que SimpleFolReasoner pour les requêtes complexes, notamment celles impliquant l’egalite.
Note: EProver est auto-detecte depuis ext_tools/EProver/eprover.exe si present. Sinon, telechargez-le depuis eprover.org.
La cellule suivante teste EProver avec des requêtes qui feraient crasher SimpleFolReasoner (heap space).
# --- 2.2.6 Test EProver pour requetes FOL avancees ---print("--- 2.2.6 Test EProver (Optionnel) ---")ifnot jvm_ready:print("ERREUR: JVM non demarree.")else: eprover_path = get_tool_path('EPROVER') if'get_tool_path'inglobals() elseNoneifnot eprover_path:print("EProver non configure. Ce test est saute.")print("Pour activer EProver, placez-le dans ext_tools/EProver/eprover.exe")else:print(f"EProver detecte: {eprover_path}")try:from org.tweetyproject.logics.fol.reasoner import EFOLReasoner, FolReasonerfrom org.tweetyproject.logics.fol.syntax import FolBeliefSet, FolSignaturefrom org.tweetyproject.logics.fol.parser import FolParserfrom org.tweetyproject.logics.commons.syntax import Sort, Constant, Predicatefrom java.util import ArrayListfrom jpype.types import JObject, JStringimport jpype FolFormula_class = jpype.JClass("org.tweetyproject.logics.fol.syntax.FolFormula")# Creer un raisonneur EProver eprover_reasoner = EFOLReasoner(JString(eprover_path))print(f"Raisonneur cree: {eprover_reasoner.getClass().getSimpleName()}")# Signature simple pour le test sig = FolSignature(True) sort_thing = Sort("Thing") sig.add(sort_thing) a = Constant("a", sort_thing) b = Constant("b", sort_thing) sig.add(a) sig.add(b)# Predicats human_arity = ArrayList() human_arity.add(sort_thing) human = Predicate("human", human_arity) mortal = Predicate("mortal", human_arity) sig.add(human) sig.add(mortal) parser = FolParser() parser.setSignature(sig) kb = FolBeliefSet()for f_str in ["human(a)", "!mortal(a) || human(a)"]: f = parser.parseFormula(f_str) kb.add(JObject(f, FolFormula_class))print(f"\nKB: {kb.size()} formules")# Requetes avec EProverprint("\nRequetes avec EProver:")for q_str in ["human(a)", "human(b)"]: q = parser.parseFormula(q_str) result = eprover_reasoner.query(kb, JObject(q, FolFormula_class))print(f" {q_str}: {result}")print("\nEProver fonctionne correctement!")exceptExceptionas e:print(f"Erreur: {e}")
Observations : - EProver confirmehuman(a) car presente dans la KB - EProver refusehuman(b) car aucune information sur b dans la KB (monde clos)
Avantages d’EProver : 1. Robustesse : Gere les quantificateurs complexes et l’egalite sans heap space 2. Performance : Champion CASC (Competition of Automated Theorem Provers) 3. Expressivite : Supporte FOL complete avec fonctions et egalite
Production : Pour des applications reelles, privilegiez EProver ou SPASS plutot que SimpleFolReasoner.
Exemple guide : Modelisation d’un problème de planification en logique propositionnelle
Contexte
Un enseignant doit planifier 3 cours (Maths, Info, Physique) sur 2 creneaux (Matin, Après-midi). Les contraintes sont : 1. Chaque cours doit etre assigne a exactement un creneau 2. Maths et Physique ne peuvent pas etre au même creneau (conflit de salle) 3. Info doit etre le matin
Objectifs
Modeliser ce problème avec des propositions Tweety (Proposition, Negation, Conjunction, Disjunction)
Construire la base de croyances (PlBeliefSet) contenant toutes les contraintes
Utiliser le raisonneur SAT (Sat4jSolver ou pySAT) pour trouver une solution
Verifier si la contrainte supplementaire “Physique le matin” est satisfiable avec les autres
“Pas au même creneau” = !(maths_am && phys_am) && !(maths_pm && phys_pm)
Reutilisez le pattern de parsing du notebook : parser_pl.parseFormula("...")
# --- Exemple guide : Planification en Logique Propositionnelle ---# Solution complete : modelisation et resolution d'un probleme de planificationif jvm_ready:from org.tweetyproject.logics.pl.syntax import Proposition, PlBeliefSet, Negation, Conjunction, Disjunctionfrom org.tweetyproject.logics.pl.sat import Sat4jSolver, SatSolverfrom org.tweetyproject.logics.pl.reasoner import SatReasoner parser = PlParser() p = parser.parseFormula kb = PlBeliefSet()for c in ("maths", "info", "phys"): kb.add(p(f"{c}_am || {c}_pm")) kb.add(Negation(p(f"{c}_am && {c}_pm"))) kb.add(p("!(maths_am && phys_am)")) kb.add(p("!(maths_pm && phys_pm)")) kb.add(p("info_am")) # Contrainte 3 : Info doit etre le matin SatSolver.setDefaultSolver(Sat4jSolver()) solver = SatSolver.getDefaultSolver()ifnot solver.isSatisfiable(kb):print("Pas de solution.")else:# Extraction d'une affectation valide : pour chaque cours on tente le# creneau "matin" et on verifie via le solveur SAT qu'il reste# consistant avec les choix deja retenus ; sinon c'est l'apres-midi.# (L'extraction directe d'un temoin par Sat4j n'etait pas fiable ici :# elle renvoyait une affectation ne satisfaisant pas la contrainte# unitaire info_am, d'ou la reconstruction pas-a-pas ci-dessous.) choix = PlBeliefSet()for f in kb: choix.add(f)for nom, am, pm in (("Maths", "maths_am", "maths_pm"), ("Info", "info_am", "info_pm"), ("Physique", "phys_am", "phys_pm")): essai = PlBeliefSet()for g in choix: essai.add(g) essai.add(p(am))if solver.isSatisfiable(essai): choix.add(p(am))print(f"{nom:10} → matin")else: choix.add(p(pm))print(f"{nom:10} → après-midi")# Contrainte supplementaire : "Physique le matin" est-elle compatible ? kb.add(p("phys_am"))if solver.isSatisfiable(kb):print("\nPhysique le matin aussi → OK")else:print("\nPhysique le matin aussi → contradiction")else:print("Exemple saute : JVM Tweety non disponible")
Maths → matin
Info → matin
Physique → après-midi
Physique le matin aussi → OK
Exercice : Planification avec contraintes supplementaires
En vous basant sur l’exemple guide ci-dessus, etendez le problème de planification avec les contraintes suivantes :
Ajoutez un 4e cours (Chimie) et un 3e creneau (Soir)
Chimie ne peut pas etre le soir (contrainte de laboratoire)
Maths et Chimie ne doivent pas etre au même creneau
Trouvez une solution satisfaisant toutes les contraintes
Reutilisez le pattern de construction KB de l’exemple guide
# --- Exercice : Planification etendue (4 cours, 3 creneaux) ---# TODO etudiant : Construisez la KB avec les 12 propositions et les contraintes# Etape 1 : Creer les propositions (maths_am, maths_pm, maths_soir, info_am, ...)# Etape 2 : Ajouter les contraintes "exactement un creneau" pour chaque cours# Etape 3 : Ajouter les contraintes d'exclusion (Chimie pas le soir, Maths != Chimie)# Etape 4 : Resoudre avec Sat4jSolver et afficher l'emploi du tempsprint("Exercice a completer")
Exercice a completer
Resume
Ce notebook a couvert: - Logique Propositionnelle (PL): Syntaxe, parsing, mondes possibles, SAT4J - Solveurs SAT Modernes: CaDiCaL, Glucose, CryptoMiniSat via pySAT (20x plus rapides que SAT4J) - Logique du Premier Ordre (FOL): Predicats, quantificateurs, signatures, EProver
Points cles: - Tweety excelle pour le parsing et la manipulation de formules logiques - pySAT complete Tweety avec des solveurs SAT haute performance - EProver est recommande pour le raisonnement FOL complexe (egalite, quantificateurs)
Prochaines étapes
Le notebook suivant explore les logiques plus avancees: Description Logic, Logique Modale, QBF.