Coverage Summary for Class: Worker (it.unibo.tuprolog.argumentation.parallel.node)

Class Method, % Branch, % Line, % Instruction, %
Worker 100% (19/19) 84.4% (81/96) 99% (200/202) 97.5% (1699/1743)
Worker$share$$inlined$sortedBy$1 0% (0/1)
Total 95% (19/20) 84.4% (81/96) 99% (200/202) 97.5% (1699/1743)


 package it.unibo.tuprolog.argumentation.parallel.node
 
 import it.unibo.tuprolog.argumentation.Flags
 import it.unibo.tuprolog.argumentation.core.Arg2pSolver
 import it.unibo.tuprolog.argumentation.core.Arg2pSolverFactory
 import it.unibo.tuprolog.argumentation.core.model.terms.ArgumentTerm
 import it.unibo.tuprolog.argumentation.core.model.terms.TermValues
 import it.unibo.tuprolog.argumentation.parallel.message.Ack
 import it.unibo.tuprolog.argumentation.parallel.message.Address
 import it.unibo.tuprolog.argumentation.parallel.message.Attack
 import it.unibo.tuprolog.argumentation.parallel.message.AttacksFound
 import it.unibo.tuprolog.argumentation.parallel.message.Build
 import it.unibo.tuprolog.argumentation.parallel.message.Candidates
 import it.unibo.tuprolog.argumentation.parallel.message.DirectAttack
 import it.unibo.tuprolog.argumentation.parallel.message.Explore
 import it.unibo.tuprolog.argumentation.parallel.message.KbMessage
 import it.unibo.tuprolog.argumentation.parallel.message.Load
 import it.unibo.tuprolog.argumentation.parallel.message.Notify
 import it.unibo.tuprolog.argumentation.parallel.message.Publish
 import it.unibo.tuprolog.argumentation.parallel.message.SharedArgument
 import it.unibo.tuprolog.argumentation.parallel.message.Subscribe
 import it.unibo.tuprolog.argumentation.parallel.message.Wanted
 import it.unibo.tuprolog.core.EmptyList
 import it.unibo.tuprolog.core.Integer
 import it.unibo.tuprolog.core.Term
 import it.unibo.tuprolog.solve.MutableSolver
 import it.unibo.tuprolog.theory.Theory
 import it.unibo.tuprolog.theory.parsing.parse
 import it.unibo.tuprolog.unify.Unificator
 import it.unibo.tuprolog.core.List as PlList
 
 /**
  * A worker of a distributed solver, at [address]: it owns some of the rules of the theory (see
  * [it.unibo.tuprolog.argumentation.parallel.partition.TheoryPartitioner]) and builds their arguments, on the arguments
  * of its own rules and on those the other workers send it, kept in its graph as well: all of them with the graph
  * builder of arg2p (see the module argumentBuilder), or, in query mode, those the evaluations depend on, explored as the
  * standard solver explores a query (see the module queryExploration). It sends each argument of its rules to the
  * workers that need its conclusion, and publishes it to the catalog; then it finds the direct attacks on the arguments
  * of its rules (see [Catalog] and [it.unibo.tuprolog.argumentation.parallel.message.KbMessage]).
  */
 internal class Worker(
     private val address: String,
     private val network: Network,
 ) : Node {
     private val index = Address.indexOf(address)
 
     // The solver of its rules, with the flags of the catalog: made as the worker is loaded, once (a graph built again has
     // workers of its own)
     private lateinit var solver: MutableSolver
     private val unificator = Unificator.default
 
     // The workers, the statements each other one needs (the premises of its rules, or those it asked to explore), and
     // the conclusions of the rules of each
     private var workers = 0
     private val interests = mutableMapOf<Int, List<Term>>()
     private val conclusionsOf = mutableMapOf<Int, List<Term>>()
 
     // Query mode: the locality explored (null on the whole graph), the statements asked to explore, and those this
     // worker asked the others to explore
     private var locality: String? = null
     private var metaConflicts = false
     private val queries = mutableListOf<Term>()
     private val asked = mutableListOf<Term>()
     private val handled = mutableSetOf<String>()
 
     // The arguments in the graph so far (the identifiers grow), those received, those of its rules; the arguments it
     // knows as terms and by their global identifiers, by their identifiers in the graph
     private var lastId = -1
     private val received = mutableSetOf<Int>()
     private val own = mutableListOf<Int>()
     private val terms = mutableMapOf<Int, Term>()
     private val globalIds = mutableMapOf<Int, String>()
 
     // The termination of the construction (Dijkstra and Scholten): the messages sent and not yet acknowledged, and the
     // node whose message engaged this worker, acknowledged once all the worker sent is
     private var deficit = 0
     private var parent: String? = null
 
     // The messages other workers sent before the Build of the catalog reached this one, by another way: handled once
     // it has, as the worker only then knows the workers to send what it builds on them
     private var building = false
     private val early = mutableListOf<KbMessage>()
 
     // The search for the attacks, again for each graph the catalog labels (in query mode it grows): started, the
     // statements each other worker wants, the arguments of the others sent to it, how many workers sent theirs
     private var attacking = false
     private var candidatesSent = false
     private var attacksSent = false
     private val wantedBy = mutableMapOf<Int, List<Term>>()
     private val candidates = mutableListOf<SharedArgument>()
 
     // The statements whose arguments may attack each argument of its rules
     private val conflictsOf = mutableMapOf<Int, List<Term>>()
     private var candidatesFrom = 0
 
     private val premises = goal("parser::classicRule(_, Body, _), member(P, Body), parser::argued(P)")
     private val heads = goal("(parser::classicRule(_, _, [C]) ; parser::classicRule(_, [C]))")
     private val explorations = goal("queryExploration::exploreAll(Q)")
     private val resumption = goal("queryExploration::resume(S)")
     private val exploredStatements = goal("(queryExploration::explored(S) ; queryExploration::premiseOfExplored(S))")
     private val kept =
         goal("argument::conclusion(A, [C]), (graphStore::saveArgument(f, [C], A) -> true ; true), graphStore::argumentId(A, Id)")
     private val fire =
         goal(
             "graphStore::forgetNewConclusions(a), graphStore::forgetNewConclusions(b), graphStore::forgetNewConclusions(f), " +
                 "argumentBuilder::buildArgumentsFromRules(N, a)",
         )
     private val built =
         goal("graphStore::argumentById(Id, A), Id > L, findall(SId, (graphStore::support(S, A), graphStore::argumentId(S, SId)), Ss)")
     private val conflicts = goal("attackCandidates::findPossibleAttackers(X, Conflict), member(S, Conflict)")
     private val attack = goal("attackRelation::attacks(T, B, X)")
 
     override fun receive(message: KbMessage) {
         when (message) {
             is Load -> {
                 load(message.theory, message.flags)
                 val needed = premises.solutions(solver).map { it.getValue("P") }.distinctBy { it.toString() }
                 val concluded = heads.solutions(solver).map { it.getValue("C") }.distinctBy { it.toString() }
                 network.send(Address.CATALOG, Subscribe(index, needed, concluded))
             }
             is Build -> {
                 engaged(Address.CATALOG) {
                     workers = message.interests.size
                     locality = message.locality
                     metaConflicts = solver.holds("setting(metaConflicts)")
                     message.interests.filter { it.worker != index }.forEach {
                         conclusionsOf[it.worker] = it.conclusions
                         // on the whole graph, the others need the arguments for their premises; in query mode, those
                         // they ask for
                         if (locality == null) interests[it.worker] = it.premises
                     }
                     if (locality == null) {
                         solver.solveOnce("argumentBuilder::buildArguments")
                         share()
                     }
                 }
                 building = true
                 early.toList().also { early.clear() }.forEach { receive(it) }
             }
             is Explore ->
                 if (!building) {
                     early += message
                 } else {
                     engaged(if (message.from < 0) Address.CATALOG else Address.worker(message.from)) {
                         if (message.from >= 0) {
                             interests[message.from] = interests[message.from].orEmpty() + message.statements
                             // the arguments for them built before: those built from now on go with the others
                             val built = own.filter { id -> message.statements.any { unificator.match(it, claimOf(terms.getValue(id))) } }
                             if (built.isNotEmpty()) {
                                 send(
                                     Address.worker(message.from),
                                     Notify(index, built.map { SharedArgument(globalIds.getValue(it), terms.getValue(it)) }),
                                 )
                             }
                         }
                         val new = message.statements.filter { statement -> queries.none { covers(it, statement) } }
                         queries += new
                         solver.solve(explorations.with("Q" to PlList.of(new))).first()
                         explored()
                     }
                 }
             is Notify ->
                 if (!building) {
                     early += message
                 } else {
                     engaged(Address.worker(message.from)) {
                         val claims = keep(message.arguments)
                         if (locality == null) {
                             solver.solve(fire.with("N" to PlList.of(claims))).first()
                             share()
                         } else {
                             resume(claims)
                         }
                     }
                 }
             is Ack -> {
                 deficit--
                 detach()
             }
             Attack -> {
                 attacking = true
                 val statements = wanted()
                 (0 until workers).filter { it != index }.forEach { network.send(Address.worker(it), Wanted(index, statements)) }
                 sendCandidates()
             }
             is Wanted -> {
                 wantedBy[message.worker] = message.statements
                 sendCandidates()
             }
             is Candidates -> {
                 candidates += message.arguments
                 candidatesFrom++
                 sendAttacks()
             }
             else -> Unit
         }
     }
 
     // The solver of the rules of the worker, [theory], with [flags], as Prolog facts (the catalog has the same)
     private fun load(
         theory: String,
         flags: String,
     ) {
         solver = Arg2pSolverFactory.solverOf(Arg2pSolver.default(listOf(Flags.parse(flags).library())), "")
         solver.loadStaticKb(Theory.parse(theory, solver.operators))
         solver.solveOnce("context_reset, parser:::convertAllRules(_)")
     }
 
     // A message from sender handled: the first one engages the worker (sender acknowledged once all the worker sent
     // is), the others are acknowledged as soon as they are handled
     private fun engaged(
         sender: String,
         handle: () -> Unit,
     ) {
         val engaging = parent == null
         if (engaging) parent = sender
         handle()
         if (!engaging) network.send(sender, Ack(index))
         detach()
     }
 
     private fun detach() {
         val engagedBy = parent
         if (deficit == 0 && engagedBy != null) {
             parent = null
             network.send(engagedBy, Ack(index))
         }
     }
 
     private fun send(
         to: String,
         message: KbMessage,
     ) {
         deficit++
         network.send(to, message)
     }
 
     // Query mode: the exploration carried on with the arguments for [claims] the others sent, which may support more of
     // the arguments of its rules; with metaConflicts, those for conflicts may make others possible attackers, explored
     // again from scratch
     private fun resume(claims: List<Term>) {
         if (metaConflicts) {
             solver.solveOnce("queryExploration::restart")
             solver.solve(explorations.with("Q" to PlList.of(queries))).first()
         } else {
             solver.solve(resumption.with("S" to PlList.of(claims))).first()
         }
         explored()
     }
 
     // Query mode, once the statements asked are explored: those the arguments built depend on as the locality goes, the
     // arguments of its rules built shared, and the others asked to explore the statements explored that their rules
     // conclude
     private fun explored() {
         if (locality == "connected") solver.solveOnce("queryExploration::expand(connected)")
         share()
         // the statements explored since the last time (by their text), with the premises of the rules for them (asked
         // all at once, so that the others build their arguments side by side: the exploration would ask for each once
         // the one before it is supported), and of those the ones not asked yet
         val needed =
             exploredStatements
                 .solutions(solver)
                 .map { it.getValue("S") }
                 .filter { handled.add(it.toString()) }
                 .filter { statement -> asked.none { covers(it, statement) } }
         asked += needed
         conclusionsOf.forEach { (worker, conclusions) ->
             val theirs = needed.filter { statement -> conclusions.any { unificator.match(it, statement) } }
             if (theirs.isNotEmpty()) send(Address.worker(worker), Explore(index, theirs))
         }
     }
 
     // The arguments of the others kept in the graph, their conclusions given
     private fun keep(arguments: List<SharedArgument>): List<Term> =
         arguments.flatMap { argument ->
             val term = argument.argument
             kept.solutions(solver, "A" to term).map {
                 val id = it.integer("Id")
                 received += id
                 terms[id] = term
                 globalIds[id] = argument.id
                 it.getValue("C")
             }
         }
 
     // The arguments of its rules built since the last time, published with their supports, and sent to the workers
     // whose rules use their conclusions
     private fun share() {
         val new = built.solutions(solver, "L" to Integer.of(lastId)).sortedBy { it.integer("Id") }
         lastId = maxOf(lastId, new.maxOfOrNull { it.integer("Id") } ?: lastId)
         val mine =
             new.filter { it.integer("Id") !in received }.map {
                 val id = it.integer("Id")
                 own += id
                 terms[id] = it.getValue("A")
                 globalIds[id] = "$index:$id"
                 val supports =
                     it.getValue("Ss").castToList().toList().map { support ->
                         globalIds.getValue((support as Integer).intValue.toInt())
                     }
                 id to SharedArgument(globalIds.getValue(id), it.getValue("A"), supports)
             }
         if (mine.isEmpty()) return
         send(Address.CATALOG, Publish(index, mine.map { it.second }))
         interests.forEach { (worker, patterns) ->
             val wanted = mine.filter { (id, _) -> patterns.any { unificator.match(it, claimOf(terms.getValue(id))) } }
             if (wanted.isNotEmpty()) {
                 send(
                     Address.worker(worker),
                     Notify(
                         index,
                         wanted.map { (_, it) ->
                             SharedArgument(it.id, it.argument)
                         },
                     ),
                 )
             }
         }
     }
 
     // The statements whose arguments may attack the arguments of its rules, found once for each (see sendAttacks)
     private fun wanted(): List<Term> {
         own.filter { it !in conflictsOf }.forEach { id ->
             conflictsOf[id] = conflicts.solutions(solver, "X" to terms.getValue(id)).map { it.getValue("S") }
         }
         return conflictsOf.values.flatten().distinctBy { it.toString() }
     }
 
     // Once the attack search started and every other worker told what it wants, each is sent the arguments of the rules
     // of this worker for it, as much of them as an attack needs: [[], TopRule, Conclusion, Body, Info]
     private fun sendCandidates() {
         if (!attacking || candidatesSent || wantedBy.size < workers - 1) return
         candidatesSent = true
         wantedBy.forEach { (worker, patterns) ->
             val matching = own.filter { id -> patterns.any { unificator.match(it, claimOf(terms.getValue(id))) } }
             network.send(
                 Address.worker(worker),
                 Candidates(index, matching.map { SharedArgument(globalIds.getValue(it), stub(terms.getValue(it))) }),
             )
         }
         sendAttacks()
     }
 
     // Once every other worker sent its candidates, the direct attacks on the arguments of its rules, by those and by the
     // arguments of its own rules, each pair checked once
     private fun sendAttacks() {
         if (!candidatesSent || attacksSent || candidatesFrom < workers - 1) return
         attacksSent = true
         val attackers =
             own.map { globalIds.getValue(it) to terms.getValue(it) } + candidates.map { it.id to it.argument }
         val attacks =
             own
                 .flatMap { id ->
                     val target = terms.getValue(id)
                     val statements = conflictsOf.getValue(id)
                     attackers
                         .filter { (_, attacker) -> statements.any { unificator.match(it, claimOf(attacker)) } }
                         .flatMap { (attackerId, attacker) ->
                             attack
                                 .solutions(solver, "B" to attacker, "X" to target)
                                 .map { DirectAttack(it.getValue("T").toString(), attackerId, globalIds.getValue(id)) }
                         }
                 }.distinct()
         network.send(Address.CATALOG, AttacksFound(index, attacks))
         // the search done: what the others send from now on is for the next one (they sent all of this one already)
         attacking = false
         candidatesSent = false
         attacksSent = false
         wantedBy.clear()
         candidates.clear()
         candidatesFrom = 0
     }
 
     // An argument as much of it as an attack needs: itself without its rules
     private fun stub(argument: Term) =
         ArgumentTerm.parts(argument)!!.let {
             ArgumentTerm.of(
                 EmptyList(),
                 it.topRule,
                 it.conclusion,
                 it.body,
                 ArgumentTerm.info(it.lastDefeasibleRules, it.defeasibleRules, it.defeasiblePremises),
             )
         }
 
     private fun goal(text: String) = Goal(text, TermValues.operators)
 
     private fun Map<String, Term>.integer(name: String) = (getValue(name) as Integer).intValue.toInt()
 }