torchmodal.epistemic¶
torchmodal.epistemic.operators ¶
Multi-agent group-knowledge operators over several accessibility relations.
Single-agent knowledge is :func:torchmodal.functional.necessity routed
through that agent's relation. This module adds the group operators that
epistemic logic needs and that a single relation cannot express:
- :func:
everybody_knows— :math:E_G\varphi = \bigwedge_{a \in G} K_a\varphi - :func:
mutual_knowledge— the bounded tower :math:E_G^k\varphi - :func:
distributed_knowledge— :math:D_G\varphi, knowledge of the pooled relation - :func:
common_knowledge— :math:C_G\varphi, the greatest fixpoint
Read the temperature warning on :func:common_knowledge before using it.
Every modal level costs at least the box width
:func:~torchmodal.functional.box_width_entropy, so an operator that iterates
to convergence drives its lower bound to exactly zero. That is a property of
graded modal logic, not a bug, but it decides which operator belongs in a loss.
and_bounds ¶
n-ary conjunction over a stack of [L, U] bounds.
The upper bound is always :math:U_\wedge = \min_i U_i. The lower bound
uses the chosen t-norm:
"godel"(default, min): :math:L_\wedge = \min_i L_i"product": :math:L_\wedge = \prod_i L_i"luk"(Łukasiewicz): :math:L_\wedge = \max(0, \sum_i L_i - (n-1))
What it bounds. All three are exact logical AND on crisp {0, 1}
inputs and are sound lower bounds on the crisp conjunction, ordered
:math:L_{\mathrm{luk}} \le L_{\mathrm{prod}} \le L_{\mathrm{godel}}.
Why Gödel is the default here. It is both the tightest of the three and
the only idempotent one. Łukasiewicz is sub-idempotent
(:math:L \wedge L < L for :math:L \in (0,1)) and amplifies each term's
deficit by n, so folding a group of 6 agents at L = 0.93 yields
0.58 rather than 0.93 — and an iterated fold, as in
:func:mutual_knowledge, reaches exactly zero at the second level and stays
there with no gradient. Choose "luk" only for a single, non-iterated
read-out where the extra looseness is wanted.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bounds
|
Tensor
|
Stack of bounds with |
required |
dim
|
int
|
Axis to fold over. Default 0. |
0
|
tnorm
|
str
|
|
'godel'
|
Returns:
| Type | Description |
|---|---|
Tensor
|
Bounds with |
Source code in torchmodal/epistemic/operators.py
everybody_knows ¶
everybody_knows(prop_bounds: Tensor, accessibilities: Tensor, group: Group = None, tau: float = 0.1, tnorm: str = 'godel', top_k: int | None = None) -> Tensor
Everybody-knows :math:E_G\varphi = \bigwedge_{a \in G} K_a\varphi.
Each agent's knowledge is the box neuron routed through that agent's
relation, then the group is folded with :func:and_bounds.
What it bounds. The lower endpoint is a sound lower bound on crisp
:math:E_G, the upper a sound upper bound; it reduces to the crisp
operator as tau -> 0. Each agent's box contributes at most
:func:~torchmodal.functional.box_width_entropy of slack.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prop_bounds
|
Tensor
|
|
required |
accessibilities
|
Tensor
|
|
required |
group
|
Group
|
Agent indices into |
None
|
tau
|
float
|
Temperature. Default 0.1. |
0.1
|
tnorm
|
str
|
Group fold; see :func: |
'godel'
|
top_k
|
int | None
|
Passed through to :func: |
None
|
Returns:
| Type | Description |
|---|---|
Tensor
|
|
Source code in torchmodal/epistemic/operators.py
mutual_knowledge ¶
mutual_knowledge(prop_bounds: Tensor, accessibilities: Tensor, group: Group = None, depth: int = 1, tau: float = 0.1, tnorm: str = 'godel', tau_schedule: TauSchedule = None, top_k: int | None = None) -> Tensor
The bounded tower :math:E_G^k\varphi — mutual knowledge to depth k.
:math:E_G^1 = E_G\varphi, :math:E_G^{k+1} = E_G(E_G^k\varphi).
This is the operator to put in a loss, not :func:common_knowledge.
The tower degrades gracefully and keeps its gradient, whereas the fixpoint
does not (see that function's warning). With tnorm="godel" and a dense
6-agent relation at tau=0.1, the lower bound runs
0.896, 0.791, 0.687, 0.583, 0.478 for k = 1..5; with "luk" it is
0.374 then exactly 0 from k = 2 on, with no gradient.
Choosing the depth. Each level costs
:func:~torchmodal.functional.box_width_entropy, so the faithful depth for
a precision eps is k* = eps / (tau * H_bar). Compute H_bar
rather than guessing it; tau_schedule keeps the accumulated slack
bounded when a deeper tower is needed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prop_bounds
|
Tensor
|
|
required |
accessibilities
|
Tensor
|
|
required |
group
|
Group
|
Agent indices. Default: all. |
None
|
depth
|
int
|
Tower depth |
1
|
tau
|
float
|
Base temperature. Default 0.1. |
0.1
|
tnorm
|
str
|
Group fold; see :func: |
'godel'
|
tau_schedule
|
TauSchedule
|
|
None
|
top_k
|
int | None
|
Passed through to :func: |
None
|
Returns:
| Type | Description |
|---|---|
Tensor
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in torchmodal/epistemic/operators.py
pooled_accessibility ¶
Pooled relation for :math:D_G: the elementwise min over the group.
Distributed knowledge is what the group would know if its members shared everything, so it is the knowledge of an agent whose accessible set is the intersection of the members' — the smaller the relation, the stronger the knowledge.
Source code in torchmodal/epistemic/operators.py
distributed_knowledge ¶
distributed_knowledge(prop_bounds: Tensor, accessibilities: Tensor, group: Group = None, tau: float = 0.1, top_k: int | None = None) -> Tensor
Distributed knowledge :math:D_G\varphi over the pooled relation.
What it bounds. A sound bracket of crisp :math:D_G, reducing to it as
tau -> 0. Because the pooled relation is contained in every member's,
:math:D_G\varphi \ge K_a\varphi \ge E_G\varphi pointwise on the lower
endpoint, up to the smoothing gap.
Source code in torchmodal/epistemic/operators.py
common_knowledge ¶
common_knowledge(prop_bounds: Tensor, accessibilities: Tensor, group: Group = None, tau: float = 0.1, tnorm: str = 'godel', tau_decay: Optional[float] = 0.5, max_depth: Optional[int] = None, tol: float = 0.0001, max_iter: int = 200) -> Tensor
Common knowledge :math:C_G\varphi, the greatest fixpoint of
:math:X \mapsto E_G(\varphi \wedge X).
.. warning::
With the default settings the lower bound is exactly zero, for every
input, and it carries no gradient. This is measured, not incidental:
every modal level costs at least
:func:~torchmodal.functional.box_width_entropy, so an iteration that
runs to convergence can only settle on the floor. At tau=0.1 with 6
agents, L = 0.0000 and U = 1.0000 for every φ and relation
tried, with dL/dA identically 0.
Consequences for users:
- **Do not put the lower bound in a loss, a metric or a figure.** Use
:func:`mutual_knowledge` at a bounded depth instead — that is what
the fixpoint approximates and it keeps its gradient.
- The **upper** bound remains informative and is the right read-out for
"is common knowledge still attainable?".
- ``tau_decay`` therefore **defaults to 0.5**, matching
:func:`~torchmodal.functional.until_graph`, so the operator is
usable as delivered. With a geometric schedule the accumulated slack
is bounded by ``tau * H / (1 - tau_decay)`` instead of growing
without limit. Measured at ``tau=0.1``, 6 agents, φ = ``[0.9, 1]``,
``tnorm="godel"``: complete 0.542, star 0.647, ring 0.680, path
0.688 — against **0.000, with exactly zero gradient**, for all four
when the schedule is disabled with ``tau_decay=None``.
- This is the same failure as the *greatest-fixpoint cliff*: iterating
a gfp down from ⊤ through a smooth ♢ loses a little each sweep, and
below roughly 0.999 edge weight there is no non-zero fixed point to
land on — measured on a 6-cycle at ``tau=0.1``, the value falls
0.955 -> 0.754 -> 0.000 as the weight goes 1.0 -> 0.999 -> 0.99.
The annealed schedule is what makes the sequence summable. Setting
``tau_decay=None`` restores the unannealed behaviour and the floor
with it.
The crisp limit is unaffected: as ``tau -> 0`` with crisp inputs the
operator recovers classical common knowledge. The floor is a
finite-temperature artefact of graded modal logic and is *not* evidence
for the Halpern–Moses coordinated-attack result, which is about
unreliable channels; cite that theorem as context, never as something
these numbers demonstrate.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prop_bounds
|
Tensor
|
|
required |
accessibilities
|
Tensor
|
|
required |
group
|
Group
|
Agent indices. Default: all. |
None
|
tau
|
float
|
Base temperature. Default 0.1. |
0.1
|
tnorm
|
str
|
Conjunction and group fold; see :func: |
'godel'
|
tau_decay
|
Optional[float]
|
Geometric temperature decay per iteration, in |
0.5
|
max_depth
|
Optional[int]
|
Stop after this many iterations instead of converging. |
None
|
tol
|
float
|
Sup-norm convergence threshold. Default 1e-4. |
0.0001
|
max_iter
|
int
|
Iteration cap. Default 200. |
200
|
Returns:
| Type | Description |
|---|---|
Tensor
|
|
Source code in torchmodal/epistemic/operators.py
253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 | |
torchmodal.epistemic.frame_axioms ¶
Frame-axiom audit for a learned accessibility relation, vacuity-corrected.
A learned relation :math:A_\theta may or may not satisfy the frame conditions
that give a modal logic its character — reflexivity (T), seriality (D),
symmetry (B), transitivity (4), the Euclidean axiom (5). Measuring that
post-hoc is the audit regime: impose nothing, train, then read the recovered
structure off the matrix.
Why a plain average is not enough for axioms 4 and 5. Both are
implications, and Łukasiewicz conjunction sends the antecedent to 0 whenever
:math:A_{uv} + A_{vw} \le 1, while :math:\mathrm{impl}(0, b) = 1 for every
b. So any triple with two weak links counts as satisfied whatever the third
link does, and a sparse or low-variance relation scores near 1 for the wrong
reason. This module therefore never returns a bare average for 4 and 5: it
reports the coverage, the non-vacuous score, and a shape-matched null, so a
score can be read as evidence rather than as an artefact of shape.
AxiomReport ¶
Bases: dict
Per-axiom audit result.
Keys: score (mean satisfaction), coverage (fraction of triples that
actually test the axiom; None for non-implication axioms),
score_non_vacuous (mean over testing triples only — None when
coverage is 0, never a meaningless 1.0), and null (the same score on
shape-matched shuffled relations).
credited is True only when the non-vacuous score clearly exceeds the
null at non-negligible coverage — the condition under which the axiom is
evidence of structure rather than of shape.
Source code in torchmodal/epistemic/frame_axioms.py
shuffled_null ¶
Mean score on shuffles shape-matched copies of A.
Each copy keeps every world's self-weight and its multiset of outgoing weights, and only permutes which targets those weights point at. The null hypothesis is therefore "this relation has no genuine structure of this kind; its score is fixed by how strong each world's links are, not by where they point".
Source code in torchmodal/epistemic/frame_axioms.py
frame_audit ¶
frame_audit(A: Tensor, shuffles: int = 100, coverage_eps: float = 1e-06, generator: Optional[Generator] = None) -> Dict[str, AxiomReport]
Audit a relation against T, D, B, 4 and 5, with vacuity correction.
Returns one :class:AxiomReport per axiom, keyed reflexive, serial,
symmetric, transitive, euclidean.
Reflexivity and seriality are plain averages over the diagonal and the row
maxima and need no correction — shuffling leaves them unchanged, so their
null is reported as None. Symmetry gets a shuffled null but has no
coverage, being a comparison rather than an implication. Transitivity and
the Euclidean axiom get all four numbers, and score_non_vacuous is
None when no triple tests the axiom.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
A
|
Tensor
|
|
required |
shuffles
|
int
|
Shape-matched null samples. Default 100. |
100
|
coverage_eps
|
float
|
A triple counts as testing the axiom when its antecedent exceeds this. Default 1e-6. |
1e-06
|
generator
|
Optional[Generator]
|
Optional |
None
|
Returns:
| Type | Description |
|---|---|
Dict[str, AxiomReport]
|
Mapping from axiom name to :class: |
Example
import torch from torchmodal.epistemic import frame_audit A = torch.eye(4) report = frame_audit(A, shuffles=5)["reflexive"] report["score"] 1.0