Causality
The causality solver answers questions of the form “was this the cause of that?” over an argumentation theory. It is a separate module, built on top of the core engine, and it adds a small set of Prolog predicates implementing counterfactual and NESS causal tests.
It is available as the Maven module causality-solver and as the npm package
@tuprolog/arg2p-causality-solver. It is also bundled in the
IDE and in the JavaScript bridge, so in those two environments it is
ready to use with no extra setup.
Getting a causality solver
From Kotlin, the entry point is the causality extension on Arg2pSolverFactory:
import it.unibo.tuprolog.argumentation.causality.causality
import it.unibo.tuprolog.argumentation.core.Arg2pSolverFactory
val solver = Arg2pSolverFactory.causality(
"""
r_0 : exposure => disease.
f_1 :=> exposure.
""".trimIndent(),
)It returns a ready-to-use 2P-Kt MutableSolver with the causality module loaded.
Predicates
All predicates live in the causality module. As with the rest of the engine, they can be called directly or
through the causality:: prefix.
| Predicate | Meaning |
|---|---|
but_for(Cause, Effect) | The counterfactual “but-for” test: succeeds if the effect would not hold had the cause been absent. |
ness(Intervention, Cause, Effect) | The NESS test, revised model. Intervention must be unbound: it is bound to the intervention under which the cause is necessary for the effect. |
ness_intervention(Intervention, Effect) | Checks the revised NESS condition for an intervention you provide. |
ness_original(Intervention, Cause, Effect) | The published formulation of the NESS test. Intervention must be unbound. |
ness_original_intervention(Intervention, Effect) | The published formulation, on a given intervention. |
ness_original/3 and ness_original_intervention/2 implement the model of Pisano, Prakken, Sartor and
Liepina (ICAIL 2025); ness/3 and ness_intervention/2 implement a revision to appear at ICAIL 2026. See
References.
ness/3andness_original/3raise a type error if their first argument is already bound. Always pass a fresh variable and read the intervention back from the solution.
Running a query
Every causality query must start from a clean evaluation context, so prefix it with context_reset:
import it.unibo.tuprolog.core.Struct
val solution = solver.solve(Struct.parse("context_reset, ness(X, exposure, disease)")).first()
println(solution.isYes)With the DSL the same query reads:
import it.unibo.tuprolog.argumentation.core.dsl.arg2pScope
arg2pScope {
solver.solve("context_reset" and "ness"(X, "exposure", "disease")).first()
}From the JavaScript bridge, pass the query as text:
arg2p.solve('causality::ness(X, a, b)', theory, flags, _ => { })A worked example
Two independent causes, each sufficient on its own — the classic case where the but-for test fails but NESS succeeds:
f_1 :=> fire_a.
f_2 :=> fire_b.
r_1 : fire_a => house_burns.
r_2 : fire_b => house_burns.Try it — the playground bundles the causality module, so the query runs there as it does on the JVM.
but_for(fire_a, house_burns) fails: removing fire_a leaves fire_b, and the house burns anyway.
ness(X, fire_a, house_burns) succeeds and binds X to the intervention it applied, here [- fire_a]: the
intervention negates the candidate cause, and the test reports the set under which that cause turns out to be
necessary for the effect.
Notes on behaviour
- The causality solver runs its own inner evaluation with grounded semantics and no graph extensions, independently of the flags you set for the outer solver.
- Graphs computed during a causality query are cached, so repeated queries against the same theory and effect are considerably faster than the first one.