Coverage Summary for Class: CausalityAnalysis (it.unibo.tuprolog.argumentation.causality.runtime)
| Class |
Class, %
|
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| CausalityAnalysis |
100%
(1/1)
|
100%
(23/23)
|
86.8%
(33/38)
|
100%
(92/92)
|
98.4%
(753/765)
|
package it.unibo.tuprolog.argumentation.causality.runtime
import it.unibo.tuprolog.argumentation.core.Arg2pSolver
import it.unibo.tuprolog.argumentation.core.Arg2pSolverFactory
import it.unibo.tuprolog.argumentation.core.dsl.arg2pScope
import it.unibo.tuprolog.argumentation.core.flags.ArgumentLabellingMode
import it.unibo.tuprolog.argumentation.core.model.Argument
import it.unibo.tuprolog.argumentation.core.model.Graph
import it.unibo.tuprolog.argumentation.core.model.LabelledArgument
import it.unibo.tuprolog.argumentation.core.model.mining.graph
import it.unibo.tuprolog.argumentation.core.model.terms.TermValues
import it.unibo.tuprolog.argumentation.core.runtime.settings.FlagsBuilder
import it.unibo.tuprolog.argumentation.core.runtime.settings.Settings
import it.unibo.tuprolog.argumentation.core.runtime.theory.RuleAnnotations
import it.unibo.tuprolog.core.Atom
import it.unibo.tuprolog.core.Clause
import it.unibo.tuprolog.core.Struct
import it.unibo.tuprolog.core.Term
import it.unibo.tuprolog.core.parsing.parse
import it.unibo.tuprolog.solve.ExecutionContext
import it.unibo.tuprolog.solve.MutableSolver
import it.unibo.tuprolog.solve.Solution
import it.unibo.tuprolog.solve.primitive.Solve
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
private const val GRAPH_CACHE_SIZE = 256
/**
* The causal analysis of a theory, for the primitives of the causality module: the graphs of the theory and of its
* interventions, labelled by a solver of its own with grounded semantics (the other flags those of the solver asking),
* and the NESS and but-for checks on them. The theory is the static knowledge base of the solver asking
* ([Solve.Request]); what it prints goes to the standard output of that solver.
*/
internal class CausalityAnalysis {
// The solver labelling the graphs, built at the first of them: a solver of arg2p not testing causality pays nothing
private var labelling: MutableSolver? = null
// The solver labelling the graphs, printing where the solver asking does
private fun solverFor(request: Solve.Request<ExecutionContext>): MutableSolver {
val output = request.context.standardOutput
val solver =
labelling ?: Arg2pSolverFactory
.solverOf(
Arg2pSolver.default(listOf(FlagsBuilder(graphExtensions = emptyList(), argumentLabellingMode = "grounded").create())),
theory = "",
output = output,
).also { labelling = it }
if (solver.standardOutput !== output) solver.setStandardOutput(output)
return solver
}
// The same theory is solved over and over (the base theory, or the same intervention in different checks): the
// graphs are only read afterwards, hence they are shared
private val graphCache = mutableMapOf<Pair<String, String>, Graph>()
/** The graph of [theory] for [query], labelled */
private fun graphOf(
request: Solve.Request<ExecutionContext>,
theory: Theory,
query: Term,
): Graph {
// the identifiers of the clauses too: the same text is a different theory with them (see RuleAnnotations)
val text = theory.toString(asPrologText = true) + theory.joinToString(",") { RuleAnnotations.of(it).orEmpty() }
val key = text to query.toString()
graphCache[key]?.let { return it }
if (graphCache.size >= GRAPH_CACHE_SIZE) graphCache.clear()
val solver = solverFor(request)
solver.resetStaticKb()
// the theory as it is, not parsed again from its text: its clauses keep their tags
solver.loadStaticKb(theory)
val solution = solver.solve(Struct.of("answerQuery", query)).first()
// an error is not a graph: raised again, and not kept
if (solution is Solution.Halt) throw solution.exception
return solver.graph().also { graphCache[key] = it }
}
private fun concludes(
argument: Argument,
statement: Term,
) = Unificator.default.match(Term.parse(argument.conclusion, TermValues.operators), statement)
private fun attackers(
argument: Argument,
graph: Graph,
): List<LabelledArgument> =
graph.attacks
.filter { it.target.identifier == argument.identifier }
.map { attack -> graph.labellings.first { attack.attacker.identifier == it.argument.identifier } }
// The arguments IN defending argument along the attacks: a grounded chain explaining its label
private fun supporters(
argument: Argument,
graph: Graph,
chain: List<String>,
): List<LabelledArgument> =
if (argument.identifier in chain) {
emptyList()
} else {
graph.labellings.filter { argument.identifier == it.argument.identifier && it.label == "in" } +
attackers(argument, graph).flatMap { attacker ->
attackers(attacker.argument, graph)
.filter { it.argument.identifier != argument.identifier }
.flatMap { supporters(it.argument, graph, chain + argument.identifier) }
}
}
// The support set of effect: the union of all its explanations
private fun supports(
graph: Graph,
effect: Term,
): Sequence<Argument> =
graph.labellings
.asSequence()
.filter { concludes(it.argument, effect) }
.flatMap { supporters(it.argument, graph, emptyList()) }
.map { it.argument }
// The theory of the solver asking, with its flags (those of its libraries too) as facts, but the semantics, which
// is that of the analysis: its own flag facts give way to them
private fun theory(request: Solve.Request<ExecutionContext>): Theory {
val theory = request.context.staticKb
val settings = Settings.of(request.context) ?: return theory
val except = setOf(ArgumentLabellingMode.predicate())
return Theory.of(
theory.unificator,
theory.clauses.filterNot { settings.states(it) } + settings.facts(request.context, except),
)
}
// The theory with the statements of intervention as axioms, each named after its statement
private fun intervened(
request: Solve.Request<ExecutionContext>,
intervention: PlList,
): Theory =
theory(request).plus(
Theory.of(
intervention.toList().map { statement ->
Clause.of(Struct.of(":->", Atom.of("fk${statement.hashCode() and Int.MAX_VALUE}"), statement))
},
),
)
/** Some argument for [statement] is IN in the grounded labelling of the theory */
fun isAccepted(
request: Solve.Request<ExecutionContext>,
statement: Term,
): Boolean = graphOf(request, theory(request), statement).labellings.any { it.label == "in" && concludes(it.argument, statement) }
private fun baseSupports(
request: Solve.Request<ExecutionContext>,
effect: Term,
): List<Term> = supports(graphOf(request, theory(request), effect), effect).map { it.termRepresentation() }.toList()
// The IN attackers, after intervention, of the supports of effect it makes OUT or UND
private fun interventionAttackers(
request: Solve.Request<ExecutionContext>,
intervention: PlList,
supports: List<Term>,
effect: Term,
): List<Argument> =
graphOf(request, intervened(request, intervention), effect).let { graph ->
graph.labellings
.filter { it.label == "out" || it.label == "und" }
.filter { a -> supports.any { b -> Unificator.default.match(a.argument.termRepresentation(), b) } }
.flatMap { a -> attackers(a.argument, graph).filter { it.label == "in" } }
.map { it.argument }
.distinct()
}
private fun properSubsets(
request: Solve.Request<ExecutionContext>,
intervention: PlList,
): List<PlList> =
arg2pScope {
request
.solve("properSubsets"(intervention, X))
.filter { it.isYes }
.map { it.substitution[X]!!.castToList() }
.toList()
}
/** The interventions removing [cause], on the rules of the graph of [effect] */
fun interventions(
request: Solve.Request<ExecutionContext>,
cause: Term,
effect: Term,
): Sequence<PlList> {
val rules =
PlList.of(
graphOf(request, theory(request), effect)
.arguments
.map { it.topRule }
.filter { it != "none" }
.map { Term.parse(it, TermValues.operators) },
)
return arg2pScope {
request
.solve("generateInterventions"(rules, cause, X))
.filter { it.isYes }
.map { it.substitution[X]!!.castToList() }
}
}
/**
* The NESS condition of ICAIL 2025: an explanation of [effect] (a grounded chain) is refuted by [intervention],
* and by none of its proper subsets
*/
fun nessOriginal(
request: Solve.Request<ExecutionContext>,
intervention: PlList,
effect: Term,
): Boolean {
val supports = baseSupports(request, effect)
if (supports.isEmpty()) return false
val attackers = interventionAttackers(request, intervention, supports, effect)
return attackers.isNotEmpty() &&
(
intervention.estimatedLength <= 1 ||
properSubsets(request, intervention).none { interventionAttackers(request, it, supports, effect).isNotEmpty() }
)
}
/**
* The revised NESS condition: an explanation of [effect] is refuted by [intervention] (an IN attacker), and no
* proper subset of it refutes it with the same attackers
*/
fun ness(
request: Solve.Request<ExecutionContext>,
intervention: PlList,
effect: Term,
): Boolean {
val supports = baseSupports(request, effect)
if (supports.isEmpty()) return false
val attackers = interventionAttackers(request, intervention, supports, effect).map { it.termRepresentation() }
return attackers.isNotEmpty() &&
(
intervention.estimatedLength <= 1 ||
properSubsets(request, intervention).none { subset ->
val others = interventionAttackers(request, subset, supports, effect).map { it.termRepresentation() }
attackers.all { a -> others.any { b -> Unificator.default.match(a, b) } }
}
)
}
/** The but-for condition: an argument for [effect] is IN, and none is after [intervention] */
fun butFor(
request: Solve.Request<ExecutionContext>,
intervention: PlList,
effect: Term,
): Boolean =
isAccepted(request, effect) &&
graphOf(request, intervened(request, intervention), effect).labellings.none {
it.label == "in" && concludes(it.argument, effect)
}
}