Skip to content

Commit 294da26

Browse files
committed
corrected import jaxtyping
1 parent b26ee69 commit 294da26

9 files changed

Lines changed: 224 additions & 14 deletions

File tree

.github/workflows/publish.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717

1818
steps:
1919
- uses: actions/checkout@v4
20-
20+
2121
- name: Set up Python
2222
uses: actions/setup-python@v5
2323
with:
@@ -31,4 +31,4 @@ jobs:
3131
run: python -m build
3232

3333
- name: Publish to PyPI
34-
uses: pypa/gh-action-pypi-publish@release/v1
34+
uses: pypa/gh-action-pypi-publish@release/v1

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,4 +89,4 @@ instance/
8989
# OS generated files
9090
.DS_Store
9191
Thumbs.db
92-
Desktop.ini
92+
Desktop.ini

.pre-commit-config.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
repos:
2+
- repo: https://github.com/pre-commit/pre-commit-hooks
3+
rev: v4.5.0
4+
hooks:
5+
- id: trailing-whitespace
6+
- id: end-of-file-fixer
7+
- id: check-yaml
8+
- id: check-added-large-files
9+
10+
- repo: https://github.com/astral-sh/ruff-pre-commit
11+
rev: v0.1.9
12+
hooks:
13+
- id: ruff
14+
args: [ --fix ] # Automatically fix what it can
15+
- id: ruff-format # The fast Black-style formatter

.ruff.toml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# .ruff.toml
2+
line-length = 100
3+
target-version = "py38"
4+
5+
[lint]
6+
# F: Pyflakes, E/W: Pycodestyle, I: Isort, N: PEP8 Naming
7+
select = ["F", "E", "W", "I", "N", "UP", "B"]
8+
9+
ignore = [
10+
"N806", # Allow non-lowercase for Tensors (e.g., X_input)
11+
"F722", # Forward annotation syntax error (fixes jaxtyping strings)
12+
"E501", # Line too long (let the formatter handle it or ignore it)
13+
]
14+
15+
[lint.isort]
16+
known-first-party = ["seq2cause"]
17+
18+
[format]
19+
quote-style = "double"
20+
21+
[lint.pyflakes]
22+
# in complex type hints
23+
extend-generics = ["jaxtyping.Float", "jaxtyping.Int"]

README.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
# seq2cause
2-
seq2cause: Turns any discrete sequence of events into a causal graph using autoregressive models (LLaMA, GPT, RNN, Mamba).
2+
seq2cause: Turns any discrete sequence of events into a causal graph using autoregressive models (LLaMA, GPT, RNN, Mamba).
33

44
[![PyPI version](https://img.shields.io/pypi/v/seq2cause.svg)](https://pypi.org/project/seq2cause/)
55
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
66
[![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
77

8-
**seq2cause** is a Python library for **Causal Discovery on Discrete Event Sequences**. It bridges the gap between Autoregressive Models (Language Models, RNN, Mambda) and Causal Discovery by treating autoregressive models as density estimators to perform parallelized CI-tests on GPUs.
8+
**seq2cause** is a Python library for **Causal Discovery on Discrete Event Sequences**. It bridges the gap between Autoregressive Models (Language Models, RNN, Mambda) and Causal Discovery by treating autoregressive models as density estimators to perform parallelized CI-tests on GPUs.
99

1010
## 🚀 Key Features
1111

@@ -29,7 +29,7 @@ Recover the causal graph from your logs in 3 lines of code.
2929
seq2cause implements the **TRACE** framework (Temporal Reconstruction via Autoregressive Causal Estimation) for the event-to-event causal discovery and **OSCAR** for the event-to-outcome. <talk abvout cmi>
3030

3131
## Graph Types
32-
You can precise the graph types, which includes [redo graph namming and parameters in packages, put time instrance, summary graph]:
32+
You can precise the graph types, which includes [redo graph namming and parameters in packages, put time instrance, summary graph]:
3333

3434
- **Event-to-Event (per sequence):** Implements the **TRACE** algorithm using Conditional Mutual Information (CMI) approximation.
3535
- **Event-to-Outcome (per sequence):** Implements the **OSCAR** algorithm which target event-to-outcome relationships using a second autoregressive models to predict outcomes.
@@ -44,13 +44,13 @@ If you use seq2cause in your research, please cite our works:
4444

4545
```bash
4646
@misc{math2026tracescalableamortizedcausal,
47-
title={TRACE: Scalable Amortized Causal Discovery from Single Sequences via Autoregressive Density Estimation},
47+
title={TRACE: Scalable Amortized Causal Discovery from Single Sequences via Autoregressive Density Estimation},
4848
author={Hugo Math and Rainer Lienhart},
4949
year={2026},
5050
eprint={2602.01135},
5151
archivePrefix={arXiv},
5252
primaryClass={cs.LG},
53-
url={https://arxiv.org/abs/2602.01135},
53+
url={https://arxiv.org/abs/2602.01135},
5454
}
5555
```
5656

@@ -78,4 +78,3 @@ url={https://openreview.net/forum?id=1HZfpuDVeW}
7878

7979
## 📄 License
8080
This project is licensed under the MIT License - see the LICENSE file for details.
81-

pyproject.toml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,16 @@ dependencies = [
2525
"captum",
2626
]
2727

28+
[project.optional-dependencies]
29+
dev = [
30+
"pytest",
31+
"ruff",
32+
"pre-commit",
33+
"pytest-cov" # For coverage reports
34+
]
35+
2836
[project.urls]
2937
"Homepage" = "https://github.com/Mathugo/seq2cause"
3038

3139
[tool.hatch.build.targets.wheel]
32-
packages = ["src/seq2cause"]
40+
packages = ["src/seq2cause"]

src/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
# src/seq2cause/__init__.py
2-
from .core import TRACE
32

4-
__version__ = "0.1.0"
3+
__version__ = "0.1.0"

src/seq2cause/core.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,9 @@
1-
class TRACE:
2-
pass
1+
class InstanceLevelCausalDiscovery:
2+
""" "
3+
This class implements instance-level causal discovery algorithm.
4+
When given a batch of discrete sequence of events, e.g., "A B C D",
5+
it identifies the instance time and summary causal graph per sequence.
6+
7+
Attributes:
8+
tfx (any): The autoregressive model used to compute next-token probabilities.
9+
"""

src/seq2cause/sampling.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import time
2+
3+
import torch
4+
from jaxtyping import Float, Int
5+
from torch import Tensor
6+
7+
8+
def uniform_sample(
9+
prob_x: Float[Tensor, "bs vocab"] | Float[Tensor, "bs L vocab"],
10+
n_samples: int = 128,
11+
cls_token_id: int | None = None,
12+
device: torch.device | None = None,
13+
) -> Int[Tensor, "bs n_samples"] | Int[Tensor, "bs n_samples L"]:
14+
"""Uniform sampling over the vocabulary for virtual do-interventions.
15+
16+
Supports both single-step distributions and full trajectory distributions.
17+
Internal logic automatically detects dimensionality to return consistent shapes.
18+
19+
Args:
20+
prob_x: Next token probabilities over the vocabulary.
21+
Can be [batch_size, vocab] or [batch_size, seq_len, vocab].
22+
n_samples: Number of samples (particles) to generate per batch element.
23+
cls_token_id: If provided, forces the first token of every sample to this ID.
24+
device: Target device for sampled tensors.
25+
26+
Returns:
27+
sampled_tokens: The discrete samples. [bs, n_samples] for 2D input
28+
or [bs, n_samples, L] for 3D.
29+
"""
30+
31+
device = device or prob_x.device
32+
33+
if prob_x.dim() == 2:
34+
# ---- Single-step intervention ----
35+
bs, vocab = prob_x.shape
36+
37+
sampled_tokens = torch.randint(low=0, high=vocab, size=(bs,), device=device)
38+
39+
if cls_token_id is not None:
40+
sampled_tokens[:] = cls_token_id
41+
42+
return sampled_tokens
43+
44+
elif prob_x.dim() == 3:
45+
# ---- Trajectory intervention ----
46+
bs, L, vocab = prob_x.shape
47+
n = n_samples
48+
49+
sampled = torch.randint(low=0, high=vocab, size=(bs, n, L), device=device)
50+
51+
# Force CLS token if needed
52+
if cls_token_id is not None:
53+
sampled[:, :, 0] = cls_token_id
54+
55+
return sampled
56+
else:
57+
raise ValueError("prob_x must be 2D or 3D tensor")
58+
59+
60+
def multinomial_sample(
61+
prob_x: Float[Tensor, "bs vocab"] | Float[Tensor, "bs L vocab"],
62+
n_samples: int = 128,
63+
cls_token_id: int | None = None,
64+
**kwargs,
65+
):
66+
"""
67+
Multinomial sampling from prob_x.
68+
69+
Args:
70+
prob_x: Next token probabilities over the vocabulary.
71+
Can be [batch_size, vocab] or [batch_size, seq_len, vocab].
72+
n_samples: Number of samples (particles) to generate per batch element.
73+
cls_token_id: If provided, forces the first token of every sample to this ID.
74+
device: Target device for sampled tensors.
75+
76+
Returns:
77+
sampled_tokens: The discrete samples. [bs, n_samples]
78+
for 2D input or [bs, n_samples, L] for 3D.
79+
"""
80+
81+
if prob_x.dim() == 2:
82+
# ---- Single-step sampling ----
83+
# prob_x: [bs, vocab]
84+
sampled_tokens = torch.multinomial(prob_x, 1)
85+
return sampled_tokens
86+
87+
elif prob_x.dim() == 3:
88+
# ---- Trajectory sampling ----
89+
bs, L, vocab = prob_x.shape
90+
n = n_samples
91+
92+
# Expand for n samples
93+
probs = prob_x.unsqueeze(1).expand(bs, n, L, vocab)
94+
probs = probs.reshape(-1, vocab) # [(bs*n*L), vocab]
95+
96+
# Sample
97+
sampled = torch.multinomial(probs, 1).squeeze(-1)
98+
sampled = sampled.view(bs, n, L)
99+
100+
# Force CLS token if needed
101+
if cls_token_id is not None:
102+
sampled[:, :, 0] = cls_token_id
103+
104+
return sampled
105+
106+
else:
107+
raise ValueError("prob_x must be 2D or 3D tensor")
108+
109+
110+
def ancestral_sampling(
111+
model: any,
112+
encoded_input: dict[str, Tensor],
113+
value: int = 64,
114+
guidance: int = 2,
115+
context: int = 10,
116+
proposal=multinomial_sample,
117+
**kwargs,
118+
):
119+
"""
120+
Standard Ancestral Sampling using a proposal function
121+
122+
Args:
123+
model: The autoregressive model to sample from.
124+
encoded_input: A dictionary containing 'input_ids' and 'attention_mask' tensors.
125+
value: Number of samples (particles) to generate per batch element.
126+
guidance: Number of initial tokens to use as conditioning context.
127+
context: Total length of the generated sequence (including guidance).
128+
proposal: The sampling function to use for generating tokens (e.g., multinomial_sample).
129+
Returns:
130+
sampled_tokens: The generated token sequences. Shape [bs*value, context].
131+
"""
132+
133+
torch.cuda.synchronize()
134+
start_time = time.time()
135+
N = value
136+
137+
with torch.no_grad():
138+
# ---- Step 1: initialize ----
139+
start_tokens = encoded_input["input_ids"][:, :guidance].to(model.device).clone()
140+
attn_mask = encoded_input["attention_mask"].to(model.device)
141+
142+
# upsample (repeat N times)
143+
start_tokens = start_tokens.unsqueeze(1).repeat(1, N, 1).reshape(-1, guidance)
144+
attn_mask = attn_mask.unsqueeze(1).repeat(1, N, 1).reshape(-1, attn_mask.size(-1))
145+
146+
for i in range(0, context - guidance):
147+
output = model(
148+
input_ids=start_tokens,
149+
attention_mask=attn_mask[:, : guidance + i].to(model.device),
150+
)
151+
# we take the last digit. Be careful if padded
152+
prob_x = torch.nn.functional.softmax(output["logits"][:, -1, :], dim=-1)
153+
random_token = proposal(prob_x)
154+
start_tokens = torch.cat([start_tokens, random_token], dim=-1)
155+
156+
torch.cuda.synchronize()
157+
elapsed = time.time() - start_time
158+
print("Ancestral Sampling - Elapsed time: ", elapsed)
159+
return start_tokens

0 commit comments

Comments
 (0)