Tweety-3 — Argumentation abstraite de Dung en C#/.NET (port natif IKVM)

Série Tweety — port C#/.NET natif (EPIC #4667). Ce notebook exploite le module arg-dung de TweetyProject sans JVM : la librairie Java est compilée vers un fat-jar Maven shade puis exécutée sur le runtime .NET via IKVM.

Navigation : Tweety-2-Basic-Logics-Csharp (propositionnel) · Tweety-2b-Semantics-Csharp (mondes possibles) · Tweety-2c-FOL-Csharp (premier ordre) · Tweety-3-Dung (ce notebook).


Objectifs pédagogiques

L’argumentation abstraite de Dung (1995) est le formalisme historique fondateur de TweetyProject. On s’affranchit de la nature des arguments pour ne retenir que leur structure d’attaque :

  • un argument est un nœud abstrait (on ne regarde pas son contenu) ;
  • une attaque a → b (« a réfute b ») est une arête orientée ;
  • un cadre d’argumentation (AF, argumentation framework) = un graphe orienté d’attaques.

La question centrale : étant donné un AF, quels arguments accepter ? Dung définit plusieurs sémantiques (ensembles d’arguments « rationnellement acceptables » = les extensions) :

Sémantique Idée Extensions
Sans conflit (CF) pas d’attaque mutuelle interne plusieurs
Admissible sans conflit + se défend contre toute attaque plusieurs
Complète (CO) admissible + contient tout argument qu’elle défend plusieurs
Fondée (GR, grounded) la plus petite extension complète une seule
Préférée (PR) une extension complète maximale (pour l’inclusion) ≥ 1
Stable (ST) bat (attaque) tout argument hors de l’extension 0 ou plusieurs

Ce notebook construit des AF en C#, calcule leurs extensions sous chaque sémantique, et teste l’acceptation (sceptique) d’un argument.

1 — Runtime IKVM : charger le module arg-dung

On installe le runtime IKVM, on fusionne l’image (base + arch), puis on charge la DLL org.tweetyproject.tweety-dung.dll (compilée côté build à partir d’un fat-jar shade embarquant arg-dung + ses dépendances transitives : graphs, commons, math, logics-fol, logics-pl).

#r "nuget: IKVM, 8.15.0"
#r "nuget: IKVM.Image, 8.15.0"
Installed Packages
  • IKVM, 8.15.0
  • IKVM.Image, 8.15.0
using System.IO;
// RID de la machine courante (win-x64, linux-x64, osx-arm64...) : IKVM.Image tire l'image native de chaque plateforme.
string ikvmVer = "8.15.0", ikvmRid = (OperatingSystem.IsWindows() ? "win" : OperatingSystem.IsMacOS() ? "osx" : "linux") + "-" + System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture.ToString().ToLowerInvariant();
string nugetRoot = Environment.GetEnvironmentVariable("NUGET_PACKAGES")
    ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages");
string ikvmBaseAny = Path.Combine(nugetRoot, "ikvm.image", ikvmVer, "ikvm", "any", "any");
string ikvmArchDir = Path.Combine(nugetRoot, "ikvm.image.runtime." + ikvmRid, ikvmVer, "ikvm", "any", ikvmRid);
string ikvmHome = Path.Combine(Path.GetTempPath(), "ikvm-home-" + ikvmVer + "-" + ikvmRid);
void IkvmCopyMerge(string src, string dst)
{
    foreach (var d in Directory.GetDirectories(src, "*", SearchOption.AllDirectories))
        Directory.CreateDirectory(d.Replace(src, dst));
    foreach (var f in Directory.GetFiles(src, "*", SearchOption.AllDirectories))
    {
        var t = f.Replace(src, dst);
        Directory.CreateDirectory(Path.GetDirectoryName(t));
        File.Copy(f, t, overwrite: true);
    }
}
if (Directory.Exists(ikvmBaseAny) && Directory.Exists(ikvmArchDir))
{
    Directory.CreateDirectory(ikvmHome);
    IkvmCopyMerge(ikvmBaseAny, ikvmHome);
    IkvmCopyMerge(ikvmArchDir, ikvmHome);
}
AppContext.SetData("IKVM.Home", ikvmHome);
Console.WriteLine("IKVM home=" + (File.Exists(Path.Combine(ikvmHome, "lib", "tzdb.dat")) ? "OK" : "MISSING"));
IKVM home=OK
#r "org.tweetyproject.tweety-dung.dll"
// Constat 3 (#5039) : la directive #r est silencieuse par defaut en .NET Interactive.
// On verifie que la DLL IKVM/Tweety referencee est bien presente et lisible (metadata).
using System.Reflection;
var tweetyDll = "org.tweetyproject.tweety-dung.dll";
var an = AssemblyName.GetAssemblyName(tweetyDll);
Console.WriteLine($"Tweety (IKVM) reference chargee : {an.Name} v{an.Version} ({new FileInfo(tweetyDll).Length / 1024 / 1024:F1} Mo).");
Tweety (IKVM) reference chargee : org.tweetyproject.tweety-dung v1.30.0.0 (8.0 Mo).

2 — Construire un cadre d’argumentation (AF)

Un AF se construit avec DungTheory (qui implémente Graph<Argument>). On y ajoute des arguments (Argument, nommés) et des attaques (Attack(attacker, attacked)).

2.1 Un premier AF : attaque simple a → b

L’argument a attaque b. Intuitivement, si on accepte a, on ne peut pas accepter b.

using org.tweetyproject.arg.dung.syntax;
using org.tweetyproject.arg.dung.semantics;
using org.tweetyproject.arg.dung.reasoner;
using System.Collections.Generic;

var a = new Argument("a");
var b = new Argument("b");

var af = new DungTheory();
af.add(a);
af.add(b);
af.add(new Attack(a, b));   // a attaque b

Console.WriteLine("AF = " + af);
Console.WriteLine("nb noeuds = " + af.getNodes().size());
AF = <{ a, b },[(a,b)]>
nb noeuds = 2

2.2 Attaque mutuelle et cycle

Quand a et b s’attaquent mutuellement, aucune des deux sémantiques « strictes » ne peut trancher seule : c’est le cas typique où plusieurs extensions coexistent (l’une accepte a, l’autre b). On construit aussi un cycle c → d → e → c.

var af2 = new DungTheory();
var x = new Argument("x");
var y = new Argument("y");
af2.add(x); af2.add(y);
af2.add(new Attack(x, y));
af2.add(new Attack(y, x));   // attaque mutuelle
Console.WriteLine("AF2 (attaque mutuelle) = " + af2);
Console.WriteLine("Bien-fondé ? " + af2.isWellFounded());   // faux : cycle d'attaque
AF2 (attaque mutuelle) = <{ x, y },[(x,y), (y,x)]>
Bien-fondé ? False

3 — Calculer les extensions

Chaque sémantique est incarnée par un raisonneur (Simple*Reasoner). Tous héritent de AbstractExtensionReasoner qui expose :

  • getModels(DungTheory) → la collection des extensions (Extension<DungTheory>) ;
  • query(DungTheory, Argument) → Boolean, acceptation sceptique de l’argument.

3.1 L’extension fondée (grounded) — unique, sceptique

L’extension fondée est la plus prudente : on part de l’ensemble vide, on ajoute tout argument non attaqué, puis tout argument défendu, et on itère jusqu’à point fixe. Elle est toujours unique.

// Sur l'AF a -> b : l'extension fondée est {a} (a est inattaqué, b est battu par a)
AbstractExtensionReasoner grounded = new SimpleGroundedReasoner();
// getModels renvoie une java.util.Collection : on itère via toArray() (foreach C# cast chaque élément).
Console.WriteLine("Extensions fondées de AF :");
foreach (Extension ext in grounded.getModels(af).toArray(new Extension[0]))
    Console.WriteLine("  " + ext);

// Acceptation sceptique : a accepté, b refusé
Console.WriteLine("\nAF, fondée ⊨ a ? " + grounded.query(af, a));
Console.WriteLine("AF, fondée ⊨ b ? " + grounded.query(af, b));
Extensions fondées de AF :
  {a}

AF, fondée ⊨ a ? true
AF, fondée ⊨ b ? false

Lecture du résultat : pourquoi {a} ?

Sur l’AF a -> b, l’extension fondée (grounded) est {a}. Cette sémantique se construit de façon sceptique et déterministe, par point fixe :

  1. On part des arguments inattaqués — a n’est attaqué par personne, donc a entre dans l’extension.
  2. Tout argument attaqué par un membre déjà accepté est battu — b, attaqué par a, est exclu.
  3. On itère : aucun nouvel argument ne devient inattaqué, donc on s’arrête.

D’où le verdict de la cellule : grounded |= a vrai, grounded |= b faux. La force de la fondée est son unicité (toujours une seule extension, jamais d’ambiguïté) et sa complexité polynomiale. Sa faiblesse est sa prudence extrême : dès que deux arguments s’attaquent mutuellement, aucun des deux n’est accepté — c’est précisément ce que la cellule suivante met en évidence.

3.2 Attaque mutuelle : fondée vs stable vs préférée

Sur l’AF à attaque mutuelle {x↔︎y} :

  • fondée : {} (vide — ni x ni y n’est défendu, le point fixe ne décolle pas) ;
  • stable : {x} et {y} (deux extensions — chaque argument bat l’autre) ;
  • préférée : {x} et {y} (deux extensions maximales admissibles).

C’est l’exemple canonique où sémantiques divergent : prudente (fondée) vs tranchantes (stable/préférée).

void Dump(string label, AbstractExtensionReasoner r, DungTheory theory)
{
    Console.WriteLine(label + " :");
    foreach (Extension ext in r.getModels(theory).toArray(new Extension[0]))
        Console.WriteLine("  " + ext);
}

Dump("AF2 fondée",   new SimpleGroundedReasoner(),  af2);
Dump("AF2 stable",   new SimpleStableReasoner(),    af2);
Dump("AF2 préférée", new SimplePreferredReasoner(), af2);
Dump("AF2 complète", new SimpleCompleteReasoner(),  af2);
AF2 fondée :
  {}
AF2 stable :
  {x}
  {y}
AF2 préférée :
  {x}
  {y}
AF2 complète :
  {x}
  {y}
  {}

Lecture du résultat : la hiérarchie des sémantiques se dévoile

Sur l’attaque mutuelle x <-> y, les quatre sémantiques divergent — c’est le moment où leur hiérarchie d’inclusions devient lisible :

Sémantique Extensions Lecture
Fondée {} Aucun argument n’est inattaqué, donc rien n’entre. Prudence maximale.
Stable {x}, {y} Accepter l’un exclut l’autre. {} n’est pas stable : un stable doit attaquer tout non-membre, or {} n’attaque personne.
Préférée {x}, {y} Maximaux parmi les admissibles. Ici, coïncide avec le stable.
Complète {x}, {y}, {} Comme la préférée plus {} : l’extension vide est admissible, et elle est complète ici car aucun argument n’est inattaqué (sur d’autres AF, {} cesse d’être complète — l’Exercice 3 le montrera).

La cascade d’inclusions canonique : fondée ⊆ complète, et la fondée est la plus petite extension complète (ici {}). stable ⊆ complète quand des stables existent ; préférée = complète maximale. C’est cette hiérarchie que les exercices ci-dessous vous demanderont d’explorer sur des AF plus subtils (cycle à trois, auto-attaque).

4 — Un AF plus riche : défense en chaîne

Construisons un AF où un argument est attaqué mais défendu par un autre :

        d
        |
        v
   c <- a -> b
  • a attaque b et c ;
  • d attaque a (donc d défend indirectement b et c).

L’extension fondée devrait contenir d (inattaqué), qui défend b et c contre a.

var A = new Argument("A");
var B = new Argument("B");
var C = new Argument("C");
var D = new Argument("D");

var af3 = new DungTheory();
af3.add(A); af3.add(B); af3.add(C); af3.add(D);
af3.add(new Attack(A, B));   // A attaque B
af3.add(new Attack(A, C));   // A attaque C
af3.add(new Attack(D, A));   // D attaque A (défend B et C)
Console.WriteLine("AF3 = " + af3);

// Extension fondée : D (inattaqué) défend B et C contre A -> {D, B, C}
AbstractExtensionReasoner gr = new SimpleGroundedReasoner();
foreach (Extension ext in gr.getModels(af3).toArray(new Extension[0]))
    Console.WriteLine("AF3 fondée = " + ext);

// Acceptation sceptique
Console.WriteLine("\nAF3 fondée ⊨ D ? " + gr.query(af3, D));
Console.WriteLine("AF3 fondée ⊨ B ? " + gr.query(af3, B));
Console.WriteLine("AF3 fondée ⊨ A ? " + gr.query(af3, A));   // battu par D
AF3 = <{ A, B, C, D },[(A,B), (A,C), (D,A)]>
AF3 fondée = {B,C,D}

AF3 fondée ⊨ D ? true
AF3 fondée ⊨ B ? true
AF3 fondée ⊨ A ? false

Lecture du résultat : la défense en chaîne

Sur AF3 = {A->B, A->C, D->A}, l’extension fondée est {B, C, D}. C’est l’illustration du mécanisme de défense qui distingue l’argumentation de la simple élimination :

  • D est inattaqué → entre (germe du raisonnement).
  • D attaque A → A est battu et exclu.
  • Une fois A exclu, ses attaques sur B et C s’évanouissent → B et C redeviennent inattaqués et entrent à leur tour.

D défend B et C en neutralisant leur attaquant commun A. C’est pourquoi grounded |= D et grounded |= B sont vrais, mais grounded |= A est faux. Un argument peut donc être accepté non parce qu’il est inattaqué, mais parce qu’il est défendu par un allié déjà accepté — ce mécanisme itératif est le cœur des systèmes d’argumentation abstraite.


Exercices

Stubs sans throw/raise (convention C.1) : le notebook s’exécute de bout en bout même non complété.

Exercice 1 — Cycle à trois : stable vs fondée

Construisez un AF en cycle p → q → r → p (chaque argument en attaque un autre, formant un 3-cycle). Affichez l’extension fondée et les extensions stables.

Question : combien d’extensions stables obtient-on ? L’extension fondée est-elle vide ? (Indice : dans un cycle impair sans argument inattaqué, le point fixe fondé ne décolle pas. Pour les stables, testez chaque candidat un à un : un singleton est stable s’il bat tous les non-membres — la situation est plus subtile que le 2-cycle x ↔︎ y de la section 3.2, où chaque singleton battait l’unique non-membre.)

// TODO etudiant : cycle p -> q -> r -> p
// Indice : declarez p,q,r, ajoutez 3 Attack, puis getModels(SimpleGroundedReasoner) et (SimpleStableReasoner).
object nbStables = null;  // TODO etudiant : nombre d'extensions stables (int)
Console.WriteLine("extensions stables du cycle = " + (nbStables ?? "Exercice a completer"));
extensions stables du cycle = Exercice a completer

Correction guidée — Exercice 1 : le 3-cycle n’a AUCUNE extension stable

Fondée : ∅, sans surprise — aucun argument n’est inattaqué, le point fixe ne décolle pas.

Stables : zéro. C’est le résultat qui surprend, alors démontrons-le sur les candidats :

  • Singletons : {p} est-il stable ? Il doit battre tous ses non-membres. p attaque q… mais pas r (c’est r qui attaque p). r est non-membre et non battu → {p} n’est pas stable. Symétriquement pour {q} et {r}.
  • Paires : {p,q} porte l’attaque interne p → q → pas sans conflit. {q,r} et {r,p} pareil. Aucune paire n’est candidate.
  • L’ensemble plein {p,q,r} n’est pas sans conflit non plus. Aucun ensemble sans conflit ne bat tout le reste : l’AF n’a aucune extension stable.

L’intuition à retenir : le 2-cycle x ↔︎ y (section 3.2) offrait deux stables parce qu’un singleton y battait l’unique non-membre. Dans un 3-cycle, chaque argument n’en bat qu’un sur deux — le cycle impair est le cas d’école de la non-existence des stables (les cycles pairs, eux, offrent les deux stables alternés : sur p→q→r→s→p, {p,r} et {q,s}). Et c’est exactement pour ça que la théorie de Dung ne repose pas sur la sémantique stable : elle peut ne rien renvoyer du tout. La préférée, elle, existe toujours (au pire ∅), et la fondée existe toujours et est unique — deux garanties d’existence que la stable n’offre pas.

Exercice 2 — Auto-attaque

Un argument qui s’attaque lui-même (s → s) est dit auto-réfuté. Construisez un AF avec un seul argument s qui s’attaque, et vérifiez que aucune sémantique admissible ne l’accepte : l’extension fondée doit être vide, et query(af, s) doit valoir false.

Indice : af.add(new Attack(s, s)).

// TODO etudiant : auto-attaque s -> s
// Indice : un seul Argument s + Attack(s,s). Puis grounded.query(af, s).
bool? accepteS = null;  // TODO etudiant
Console.WriteLine("AF auto-attaque, fondée ⊨ s ? " + (accepteS?.ToString() ?? "Exercice a completer"));
AF auto-attaque, fondée ⊨ s ? Exercice a completer

Correction guidée — Exercice 2 : l’auto-attaque exclut partout

Le verdict attendu tombe : fondée = ∅, et query(af, s) = false. La raison tient en une définition : une extension doit être sans conflit, et un ensemble contenant s porte l’attaque s → s — le conflit est interne à l’argument lui-même. s ne peut donc figurer dans aucune extension, pour aucune sémantique (fondée, complète, préférée, stable : toutes exigent l’absence de conflit interne).

Deux observations bonus, à vérifier avec Dump :

  1. Aucune extension stable non plus : le seul candidat, ∅, devrait battre s pour être stable — or ∅ n’attaque personne. L’AF à auto-attaque est un deuxième exemple (avec le 3-cycle de l’Exercice 1) d’inexistence des stables.
  2. Complète = préférée = {∅} : l’extension vide est trivialement admissible (aucun conflit, rien à défendre) et complète. C’est le plancher commun de toutes les sémantiques.

En pratique, l’auto-attaque est la manière la plus économique de rendre un argument inacceptable : le moteur de raisonnement n’a même pas besoin d’examiner le reste du graphe. Dans les cadres structurés (Tweety-4, Aspic+), une prémisse contradictoire ou une règle auto-réfutée se ramène souvent à ce schéma abstrait.

Exercice 3 — Complète vs préférée… et le statut de l’extension vide

Sur l’AF {a → b, a → c} (où a attaque b et c, mais rien n’attaque a), affichez les extensions complètes et préférées, puis répondez aux deux questions :

  1. Quelle est l’extension commune aux deux sémantiques — et sont-elles ici confondues ?
  2. Sur le 2-cycle de la section 3.2, {} figurait parmi les complètes. Est-elle encore complète ici ? (Indice : une extension complète doit contenir tout argument qu’elle défend ; qui {} défend-elle quand a est inattaqué ?)

Seconde question-piège : {a, b} est-elle admissible ? (b et c ne s’attaquent pas mutuellement — mais est-ce la bonne question à se poser ?)

// TODO etudiant : AF {a -> b, a -> c}, extensions completes vs preferees
// Indice : utilisez Dump("complete", new SimpleCompleteReasoner(), af) et ("preferee", new SimplePreferredReasoner(), af).
string verdict = null;  // TODO etudiant : quelle extension est commune aux deux sémantiques ?
Console.WriteLine("extension commune complète/préférée = " + (verdict ?? "Exercice a completer"));
extension commune complète/préférée = Exercice a completer

Correction guidée — Exercice 3 : pourquoi {} n’est PAS complète ici

Le décompte attendu :

Sémantique Extensions
Fondée {a}
Complète {a} seulement
Préférée {a}
Stable {a}

(1) Complète et préférée sont confondues : {a} — l’extension commune. Sur cet AF, la complète n’admet aucune extension non-maximale en plus de la préférée.

(2) Le résultat le plus instructif : contrairement au 2-cycle de la section 3.2, {} n’est pas complète. Rappel de la définition : E est complète si E est admissible et contient tout argument qu’elle défend. Or « défendre » un argument sans attaquants est une condition vide — {} défend donc a (inattaqué), et une extension complète devrait alors contenir a. {} ne le contient pas : elle n’est pas complète. Retenez la règle exacte : {} est admissible sur tout AF, mais complète seulement si aucun argument n’est inattaqué — c’était le cas du 2-cycle (chaque argument y est attaqué), pas ici.

La question-piège : {a, b} n’est pas admissible — et la raison n’est pas l’absence d’attaque entre b et c, mais l’attaque a → b interne à l’ensemble : un ensemble sans conflit ne peut pas contenir un argument et sa cible. Quant à b et c, ils sont battus par a et personne ne les défend (défendre b exigerait d’attaquer a — rien ne l’attaque) : ils restent hors de toute extension non vide.

5 — Synthèse : quatre sémantiques, cinq graphes, une grille de lecture

Les AF rencontrés dans ce notebook se rangent en une grille unique — la garder sous les yeux vaut toutes les définitions :

AF Fondée Complète Préférée Stable Ce que l’AF enseigne
a → b {a} {a} {a} {a} cas dégénéré : tout coïncide
x ↔︎ y (2-cycle) ∅ {x},{y},∅ {x},{y} {x},{y} la fondée se trompe par prudence
A→B, A→C, D→A (chaîne) {B,C,D} {B,C,D} {B,C,D} {B,C,D} la défense réhabilite les battus
p→q→r→p (3-cycle) ∅ ∅ ∅ aucune cycle impair : la stable disparaît
s → s (auto-attaque) ∅ ∅ ∅ aucune l’inacceptable universel

Une phrase par sémantique : la fondée est le point fixe sceptique — unique, calculable en polynomial, mais parfois trop prudente (x ↔︎ y). La complète ajoute à l’admissibilité la règle « tout argument défendu est accepté » — c’est la plus fidèle à la notion de position rationnelle. La préférée en prend les maximaux — le choix standard quand on veut TOUTES les positions maximales défendables. La stable exige de battre activement chaque outsider — la plus agressive, et la seule sans garantie d’existence.

Le rapport d’inclusion qui structure tout : fondée ⊆ chaque complète ⊆ quelque préférée, et ∅ est admissible partout mais complète seulement sans argument inattaqué (comparez les lignes a → b et x ↔︎ y — c’est la leçon de l’Exercice 3), stable ⊆ préférée quand des stables existent (les deux dernières lignes du tableau montrent ce qui arrive sinon). Les trois garanties de Dung (1995) à retenir : la fondée existe et est unique ; la préférée existe toujours ; la stable n’est garantie nulle part.

Choisir en pratique : position unique et incontestable → fondée ; énumérer les lectures maximales d’un débat → préférée ; modéliser un système où tout non-accepté doit être « réfuté » (configurations, game theory) → stable, en vérifiant d’abord qu’elle existe. La suite naturelle dans la série : Tweety-4-Aspic-Csharp (argumentation structurée : les arguments deviennent des déductions, les attaques des rebuts/undercuts) et Tweety-7a-Extended-Frameworks-Csharp (attaques sur attaques, bipolarité).


Conclusion

On a porté en C#/.NET natif (sans JVM) l’argumentation abstraite de Dung — le cœur historique de TweetyProject — via le module arg-dung et IKVM :

  1. Construction d’un cadre d’argumentation (DungTheory + Argument + Attack), y compris attaques mutuelles, cycles et défense indirecte.
  2. Calcul des extensions sous les quatre sémantiques fondatrices : fondée (unique, sceptique), complète, préférée (maximale), stable (agressive) — via getModels.
  3. Acceptation sceptique d’un argument via query.

Pourquoi plusieurs sémantiques ?

Sémantique Stratégie Quand l’utiliser
Fondée la plus prudente (point fixe) décision unique, conservatrice
Complète tout argument défendu est inclus fermeture « naturelle »
Préférée maximale pour l’inclusion « le mieux possible » sans arbitrer
Stable bat tout l’extérieur verdicts nets, mais peut ne pas exister

Références

  • Dung, P. M. On the Acceptability of Arguments and its Fundamental Rôle in Nonmonotonic Reasoning, Logic Programming and n-Person Games. Artificial Intelligence, 1995.
  • TweetyProject — arg-dung module.
  • Port C#/.NET via IKVM — EPIC #4667.
Retour au sommet