Skip to main content

PyPI CI License: MIT Python

Differentiable Modal Logic for PyTorch

A PyTorch library implementing Modal Logical Neural Networks (MLNNs) — the first framework enabling differentiable reasoning over necessity and possibility by integrating neural networks with Kripke semantics from modal logic.

Installation

pip install torchmodal

Or from a checkout (recommended while a release is pending, since features land here first):

pip install -e .

See CHANGELOG.md for what each release contains.

Quick Start

import torch
import torchmodal
from torchmodal import nn, KripkeModel

# Create a 3-world Kripke model with learnable accessibility
model = KripkeModel(
    num_worlds=3,
    accessibility=nn.LearnableAccessibility(3, init_bias=-2.0),
    tau=0.1,
)

# Add propositions
model.add_proposition("safe", learnable=True)
model.add_proposition("online", learnable=False)

# Evaluate modal operators
A = model.get_accessibility()
box_safe = model.necessity("safe", A)       # □(safe) — necessarily safe
dia_online = model.possibility("online", A)  # ♢(online) — possibly online

# Compute contradiction loss
loss = model.contradiction_loss()

Architecture

torchmodal/
├── __init__.py          # Public API
├── functional.py        # Stateless functional operators (like torch.nn.functional)
├── nn/
│   ├── operators.py     # SmoothMin, SmoothMax, ConvPool modules
│   ├── connectives.py   # Negation, Conjunction, Disjunction, Implication
│   ├── modal.py         # Necessity (□), Possibility (♢) neurons
│   └── accessibility.py # Fixed, Learnable, Metric, Attention accessibility relations
├── kripke.py            # KripkeModel, Proposition
├── losses.py            # ContradictionLoss, ModalLoss, SparsityLoss, CrystallizationLoss, SemanticLoss
├── inference.py         # Upward-downward bound propagation
├── systems.py           # EpistemicOperator, DoxasticOperator, TemporalOperator, MultiAgentKripke
└── utils.py             # Temperature annealing, accessibility builders, decoding

Core Concepts

Differentiable Kripke Semantics

A Kripke model M = ⟨W, R, V⟩ is realized as differentiable tensors:

  • W (Worlds): A finite set of possible worlds — agents, time steps, or contexts
  • R (Accessibility): A relation determining which worlds can "see" each other
  • V (Valuation): Truth bounds [L, U] ⊆ [0, 1] for each proposition in each world

Modal Operators

Operator Symbol Semantics Implementation
Necessity True in all accessible worlds smooth_min over weighted implications
Possibility True in some accessible world smooth_max over weighted conjunctions
Until U ϕ holds until ψ becomes true backward DP sweep U_t = ψ_t ∨ (ϕ_t ∧ U_{t+1})
Knowledge K_a Agent a knows ϕ □ restricted to agent's row
Belief B_a Agent a believes ϕ □ with non-reflexive access
Globally G ϕ at all future times □ over temporal accessibility
Finally F ϕ at some future time ♢ over temporal accessibility

Aggregators are named smooth_min / smooth_max (not softmin / softmax) to avoid confusion with the probability-normalizing torch.softmax; the old names remain as deprecated aliases.

Top-k aggregation is a parameter of the operators — nn.Necessity(tau, top_k=k), nn.Possibility(tau, top_k=k), functional.necessity(..., top_k=k) — not of the accessibility modules. Each endpoint keeps the k extreme aggregation terms (the k smallest of (1 − A) + L for L_□, the k largest of A + U − 1 for U_♢, …) and aggregates only those, so the true min/max is always kept, the bounds stay sound, the smooth endpoints are within τ·log k of the crisp value, and nothing depends on |W|. Masking A by its k largest entries before aggregation (the top_k= of the accessibility modules up to 0.2.0) was unsound and is deprecated — see the CHANGELOG.

Accessibility Relations

# Fixed (deductive mode): enforce known rules
R = torchmodal.build_sudoku_accessibility(3)
access = nn.FixedAccessibility(R)

# Learnable direct matrix (small worlds)
access = nn.LearnableAccessibility(num_worlds=7, init_bias=-2.0)

# Metric learning (scales to 20,000+ worlds)
access = nn.MetricAccessibility(num_worlds=10000, embed_dim=64)

# Attention-based (rich per-world features, asymmetric relations)
access = nn.AttentionAccessibility(input_dim=384, num_heads=4)
A = access(features)  # features: (num_worlds, 384)

# Deliberately sparsified relation (a modelling choice: each world keeps only
# its k most accessible worlds, the rest become inaccessible). This is NOT an
# aggregation optimisation — for that use top_k= on the operators.
access = nn.MetricAccessibility(num_worlds=10000, embed_dim=64, sparsify=8)

Loss Functions

# Combined modal loss: L_total = L_task + β * L_contra
criterion = torchmodal.ModalLoss(beta=0.3)
loss = criterion(task_loss, model.all_bounds())

# Sparsity regularization on accessibility
sparse_loss = torchmodal.SparsityLoss(lambda_sparse=0.05)

# Crystallization for SAT mode (forces crisp 0/1 assignments)
crystal_loss = torchmodal.CrystallizationLoss()

# Semantic Loss baseline (Xu et al. 2018), incl. mutual-exclusion constraints
sem = torchmodal.SemanticLoss()
loss = sem.forward_mutual_exclusive(probs)  # "exactly one of k" per row

Examples

All examples are self-contained scripts in examples/ and can be run directly:

python examples/sudoku.py
Example Modal Logic Description
sudoku.py □, CSP 4x4 Sudoku via modal contradiction + crystallization
temporal_epistemic.py K, G, F, K∘G Learns epistemic accessibility to resolve contradictions
epistemic_trust.py K_a Trust learning from promise-keeping behavior
doxastic_belief.py B_a Belief calibration and hallucination detection
temporal_causal.py □(cause → crash) Root cause analysis in event traces
deontic_boundary.py O, P Normative boundary learning (spoofing detection)
trust_erosion.py Temporal + Deontic Retroactive lie detection collapses trust
dialect_classification.py □, ♢ thresholds OOD detection — 89% Neutral recall trained only on AmE/BrE
axiom_ablation.py T, 4, B axioms Effect of reflexivity/transitivity/symmetry on structure learning
scalability_ring.py □, ♢ Ring structure recovery with tau/top-k/learnable ablation
graph_coloring_benchmark.py ⋀_c(p_c → ¬♢p_c) 12-solver comparison on planted-colourable graphs + inductive constraint-graph recovery (edge AUC 1.0)
sudoku_benchmark.py □, CSP Sudoku solver benchmark (peer-graph special case of colouring)
baseline_comparison.py Side-by-side differentiable baselines (Semantic Loss, soft non-modal penalty)
MLNN_AccesbilityScalabilityAblation.ipynb □, ♢ Dense vs. metric accessibility sweep, N = 20 → 20,000 worlds on one GPU

Epistemic Trust Learning (CaSiNo / Diplomacy)

from torchmodal import MultiAgentKripke

# 7 agents (Diplomacy powers), 3 time steps
kripke = MultiAgentKripke(
    num_agents=7,
    num_steps=3,
    learnable_epistemic=True,
    init_bias=-2.0,
)

# Evaluate "agent knows claim is consistent over time"
K_G_claim = kripke.K_G(claim_bounds)

# Learn trust from contradiction minimization
A = kripke.get_epistemic_accessibility()

Sudoku as Constraint Satisfaction

import torchmodal
from torchmodal import KripkeModel, nn

# 81 worlds (cells), fixed Sudoku accessibility
R = torchmodal.build_sudoku_accessibility(3)
model = KripkeModel(
    num_worlds=81,
    accessibility=nn.FixedAccessibility(R),
)

# 9 propositions (digits)
for d in range(1, 10):
    model.add_proposition(f"d{d}", learnable=True)

# Train with contradiction loss + crystallization
contra_loss = torchmodal.ContradictionLoss(squared=True)
crystal_loss = torchmodal.CrystallizationLoss()

POS Tagging with Grammatical Guardrails

from torchmodal import nn, functional as F

# 3-world structure: Real, Pessimistic, Exploratory
box = nn.Necessity(tau=0.1)
access = nn.LearnableAccessibility(3)

# Enforce axiom: □¬(DET_i ∧ VERB_{i+1})
A = access()
det_bounds = ...   # from proposer network
verb_bounds = ...
conj = F.conjunction(det_bounds, verb_bounds)
neg_conj = F.negation(conj)
box_constraint = box(neg_conj, A)  # must be high (true)

Formula Graph Inference

from torchmodal import FormulaGraph, upward_downward

graph = FormulaGraph()
graph.add_atomic("p")
graph.add_atomic("q")
graph.add_conjunction("p_and_q", "p", "q")
graph.add_necessity("box_p_and_q", "p_and_q")

# Initialize bounds
bounds = {
    "p": torch.tensor([[0.8, 1.0], [0.3, 0.5], [0.9, 1.0]]),
    "q": torch.tensor([[0.7, 0.9], [0.6, 0.8], [0.4, 0.6]]),
    "p_and_q": torch.tensor([[0.0, 1.0], [0.0, 1.0], [0.0, 1.0]]),
    "box_p_and_q": torch.tensor([[0.0, 1.0], [0.0, 1.0], [0.0, 1.0]]),
}

# Run inference
A = torch.eye(3)  # reflexive accessibility
tightened = upward_downward(graph, bounds, A, tau=0.1)

The upward pass evaluates every node type. The downward pass inverts each connective on both endpoints and each modal operator on the one endpoint that factorises per world:

node downward rule
¬a both endpoints (exact — negation is an involution)
a ∧ b both: L_a ← L_φ, U_a ← U_φ + 1 − L_b
a ∨ b both: L_a ← L_φ − U_b, U_a ← U_φ
a → b L_b ← L_φ + L_a − 1 (modus ponens; no modus tollens)
□ϕ lower only: L_ϕ[w'] ← max_w (L_φ[w] − 1 + A[w,w'])
♢ϕ upper only: U_ϕ[w'] ← min_w (U_φ[w] + 1 − A[w,w'])
ϕ U ψ none — the backward DP couples every time step

upper and lower are not inverted: they bound an aggregate without saying which neighbour realises it, so no canonical per-world constraint exists. The two passes are iterated to convergence_threshold, not run once — a downward update can stale a sibling formula that shares a leaf — and a RuntimeWarning is raised if max_iterations is exhausted first.

Two Learning Modes

Mode Fixed Learned Use Case
Deductive Accessibility R Propositions V POS guardrails, Sudoku, OOD detection
Inductive Propositions V Accessibility A_θ Trust learning, social structure discovery

Citation

If you use torchmodal in your research, please cite:

@misc{sulc2025modallogicalneuralnetworks,
  title={Modal Logical Neural Networks},
  author={Antonin Sulc},
  year={2025},
  eprint={2512.03491},
  archivePrefix={arXiv},
  primaryClass={cs.LG},
  url={https://arxiv.org/abs/2512.03491},
}

License

MIT

Authors

Antonin Sulc

Media

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

torchmodal-0.2.1.tar.gz (54.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

torchmodal-0.2.1-py3-none-any.whl (44.8 kB view details)

Uploaded Python 3

File details

Details for the file torchmodal-0.2.1.tar.gz.

File metadata

  • Download URL: torchmodal-0.2.1.tar.gz
  • Upload date:
  • Size: 54.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.6

File hashes

Hashes for torchmodal-0.2.1.tar.gz
Algorithm Hash digest
SHA256 8f601bb92e1ea344ce7b82dc66c31af6c064e6c2ec27518eddb421073e211435
MD5 52482470f25e98602857f569eb049aa2
BLAKE2b-256 62edc03ed8651f6ba6fb9627c1c4a7c0d5ea047c41682f18d4c9fe6a4ab337e8

See more details on using hashes here.

File details

Details for the file torchmodal-0.2.1-py3-none-any.whl.

File metadata

  • Download URL: torchmodal-0.2.1-py3-none-any.whl
  • Upload date:
  • Size: 44.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.6

File hashes

Hashes for torchmodal-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 71c6daf8a24b99ef5e3562acb176ff16ad78004bac23d2567091056812b6d27a
MD5 32eab98c39a7fbbf5c781f7abb8c0846
BLAKE2b-256 888f994bfcb2f3c809b1b5b82c5808c05637e408c698faa26b58fa6a39bcfaec

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.2.1 This release

2 files

0.2.0

2 files

0.1.1

2 files

0.1.0.6

2 files

0.1.0.5

2 files

0.1.0.4

2 files

0.1.0.3

2 files

0.1.0.2

2 files

0.1.0.1

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page