Skip to content

Commit d9ae001

Browse files
authored
Merge pull request #5 from thisiscam/pip-git-installable
Public-readiness: pip-git install + MathJax docs + uv CI (fast/slow split)
2 parents 127e372 + 4007f5f commit d9ae001

16 files changed

Lines changed: 175 additions & 106 deletions

‎.github/workflows/test-full.yml‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Full suite, including `slow` tests (deep nesting / e2e sampling / high-d).
2+
# These are deselected from the PR gate (test.yml) because the whole suite
3+
# exceeds GitHub's 6h job limit on 2-core runners. Runs nightly + on demand.
4+
name: tests-full
5+
6+
on:
7+
schedule:
8+
- cron: "0 7 * * *" # 07:00 UTC daily
9+
workflow_dispatch:
10+
11+
jobs:
12+
test-full:
13+
runs-on: ubuntu-latest
14+
timeout-minutes: 350
15+
strategy:
16+
fail-fast: false
17+
matrix:
18+
python-version: ["3.11", "3.12"]
19+
steps:
20+
- uses: actions/checkout@v4
21+
- uses: astral-sh/setup-uv@v5
22+
with:
23+
python-version: ${{ matrix.python-version }}
24+
enable-cache: true
25+
- name: Install
26+
run: uv sync --extra test --locked
27+
- name: Run full suite
28+
run: uv run --extra test python -m pytest -v -n auto

‎.github/workflows/test.yml‎

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
# Requires jet-array to be installable from PyPI.
2-
# Publish jet-array first (tag v0.1.0 on thisiscam/jet-array → CI auto-publishes).
1+
# Fast PR/push gate. Installs with uv so the oryx git dependency + tfp-nightly
2+
# resolve correctly (plain pip pulls broken stock oryx). Runs only the fast
3+
# tests; the heavy `slow`-marked suite runs in test-full.yml, since the full
4+
# suite exceeds GitHub's 6h job limit on 2-core runners.
35
name: tests
46

57
on:
@@ -14,16 +16,14 @@ jobs:
1416
strategy:
1517
fail-fast: false
1618
matrix:
17-
python-version: ["3.10", "3.11", "3.12"]
19+
python-version: ["3.11", "3.12"] # pyproject requires-python = ">=3.11"
1820
steps:
1921
- uses: actions/checkout@v4
20-
- uses: actions/setup-python@v5
22+
- uses: astral-sh/setup-uv@v5
2123
with:
2224
python-version: ${{ matrix.python-version }}
23-
cache: pip
25+
enable-cache: true
2426
- name: Install
25-
run: |
26-
python -m pip install --upgrade pip
27-
pip install -e ".[test]"
28-
- name: Run tests
29-
run: pytest -v
27+
run: uv sync --extra test --locked
28+
- name: Run tests (fast)
29+
run: uv run --extra test python -m pytest -v -m "not slow" -n auto

‎README.md‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,13 @@ AD* (arXiv:TBD).
1717

1818
## Install
1919

20-
Install from source with [uv](https://docs.astral.sh/uv/):
20+
```bash
21+
pip install git+https://github.com/thisiscam/acopula
22+
```
23+
24+
acopula depends on a patched [`oryx`](https://github.com/jax-ml/oryx) build
25+
(pulled from git as part of the install) because stock `oryx` is incompatible
26+
with jax 0.8. For development, clone and use [uv](https://docs.astral.sh/uv/):
2127

2228
```bash
2329
git clone https://github.com/thisiscam/acopula
@@ -26,12 +32,9 @@ uv sync # add --extra examples for the plotting examples
2632
uv run python examples/01_quickstart.py
2733
```
2834

29-
uv is required (for now) because `acopula` depends on a patched
30-
[`oryx`](https://github.com/jax-ml/oryx) build pinned via `[tool.uv.sources]`;
31-
plain `pip install` — including `pip install git+…` — ignores that pin, pulls
32-
the stock PyPI `oryx` (incompatible with jax 0.8), and fails at import. A
33-
`pip install acopula` from PyPI is **not available yet**; it is gated on an
34-
upstream `oryx` release compatible with jax 0.8.
35+
A PyPI `pip install acopula` is **not available yet**: the git-pinned `oryx`
36+
dependency can't be uploaded to PyPI, so this is gated on an upstream `oryx`
37+
release compatible with jax 0.8.
3538

3639
`acopula` pins `jax>=0.8,<0.9` because it relies on JAX-internal
3740
APIs in the [jet-array](https://github.com/thisiscam/jet-array) backend.

‎acopula/core.py‎

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,20 @@ def __exit__(self, *args):
4242

4343

4444
class Copula:
45-
"""Base class for Archimedean copulas in φ-notation.
45+
r"""Base class for Archimedean copulas in $\varphi$-notation.
4646
47-
Subclasses must implement generator(u) which returns φ(u), where
48-
u ∈ (0, 1], φ: (0,1] → [0, ∞), strictly decreasing, convex, φ(1)=0.
47+
Subclasses must implement `generator(u)`, which returns $\varphi(u)$,
48+
where $u \in (0, 1]$ and $\varphi:(0,1] \to [0,\infty)$ is strictly
49+
decreasing and convex with $\varphi(1)=0$.
4950
"""
5051

5152
def generator(self, u: jax.Array) -> jax.Array: # pragma: no cover - abstract
52-
"""Return the Archimedean generator φ(u).
53+
r"""Return the Archimedean generator $\varphi(u)$.
5354
54-
Subclasses **must** implement this. φ maps (0, 1] → [0, ∞), is
55-
strictly decreasing and convex, with φ(1) = 0. Everything else
56-
(density, inverse, sampling) is derived from it automatically.
55+
Subclasses **must** implement this. $\varphi$ maps $(0,1] \to
56+
[0,\infty)$, is strictly decreasing and convex, with $\varphi(1)=0$.
57+
Everything else (density, inverse, sampling) is derived from it
58+
automatically.
5759
"""
5860
raise NotImplementedError
5961

@@ -62,40 +64,41 @@ def generator_inv(self, t: jax.Array) -> jax.Array:
6264
return _invert_generator(self.generator, t)
6365

6466
def log_generator_kth_derivative(self, t: jax.Array, k: int) -> jax.Array:
65-
"""Return log |ψ^{(k)}(t)|, the log absolute k-th derivative.
67+
r"""Return $\log|\psi^{(k)}(t)|$, the log absolute $k$-th derivative.
6668
6769
Override this with a closed-form expression to bypass jet for
68-
specific families. For example, Clayton can use::
70+
specific families. For example, Clayton can use
6971
70-
lgamma(k + 1/theta) - lgamma(1/theta) - (k + 1/theta) * log1p(t)
72+
$$\log\Gamma(k + 1/\theta) - \log\Gamma(1/\theta)
73+
- (k + 1/\theta)\,\log(1+t).$$
7174
72-
The default returns None, which tells the framework to use
75+
The default returns ``None``, which tells the framework to use
7376
Taylor-mode AD via jet.
7477
"""
7578
return None # sentinel: use jet
7679

7780
def generator_taylor_coefficients(
7881
self, t: jax.Array, k_max: int
7982
) -> jax.Array:
80-
"""Return Taylor coefficients ``[ψ(t), ψ'(t)/1!, …, ψ^{(k_max)}(t)/k_max!]``.
83+
r"""Return Taylor coefficients $[\psi(t),\, \psi'(t)/1!,\, \ldots,\, \psi^{(k_{\max})}(t)/k_{\max}!]$.
8184
82-
Override this when the closed-form ψ has a numerically stable
85+
Override this when the closed-form $\psi$ has a numerically stable
8386
series representation that avoids the cancellations Taylor-mode
8487
AD on the closed form would produce at high derivative order.
85-
For example, AMH's ψ is the Laplace transform of Geometric(1−θ),
86-
so all Taylor coefficients are sums of same-sign terms (no
87-
cancellation):
88+
For example, AMH's $\psi$ is the Laplace transform of
89+
$\mathrm{Geometric}(1-\theta)$, so all Taylor coefficients are
90+
sums of same-sign terms (no cancellation):
8891
89-
ψ^{(k)}(t) = (-1)^k Σ_{x=1}^∞ x^k (1−θ) θ^{x−1} e^{−tx}
92+
$$\psi^{(k)}(t) = (-1)^k \sum_{x=1}^{\infty} x^k\,(1-\theta)\,\theta^{x-1}\,e^{-tx}.$$
9093
9194
The framework consults this hook everywhere it would otherwise
9295
call ``jet_array.jet`` on ``self.generator`` to get a Taylor
9396
expansion: the root density (``bell._root_assembly``) and the
9497
nesting composition (``compose.compute_composition_taylor``).
9598
Returning ``None`` (the default) tells those call sites to
9699
fall back to the jet path, which is correct but loses
97-
precision when ψ's derivatives have large alternating-sign
98-
cancellations (e.g. AMH past d≈30 in float64).
100+
precision when $\psi$'s derivatives have large alternating-sign
101+
cancellations (e.g. AMH past $d \approx 30$ in float64).
99102
100103
Args:
101104
t: scalar input point.
@@ -111,11 +114,11 @@ def generator_taylor_coefficients(
111114
def log_generator_taylor_coefficients(
112115
self, t: jax.Array, k_max: int
113116
) -> Optional[Tuple[jax.Array, jax.Array]]:
114-
"""Log-space variant of :meth:`generator_taylor_coefficients`.
117+
r"""Log-space variant of :meth:`generator_taylor_coefficients`.
115118
116119
Return ``(sign, log_abs)``, two arrays of shape ``(k_max + 1,)`` with
117120
118-
ψ^{(k)}(t) / k! == sign[k] * exp(log_abs[k]).
121+
$$\psi^{(k)}(t) / k! \;=\; \mathrm{sign}[k]\,\exp(\mathrm{log\_abs}[k]).$$
119122
120123
The Bell density consumes the Taylor coefficients as ``(sign, log|c|)``
121124
pairs internally, so a family whose normalized coefficients are

‎docs/api.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ on its children to build the model tree.
3636
## Composition registry
3737

3838
For nested copulas that mix families, register a closed-form composition
39-
`ψ_outer⁻¹ ∘ ψ_inner`, or choose the fallback strategy used when none is
40-
registered.
39+
$\psi_{\text{outer}}^{-1} \circ \psi_{\text{inner}}$, or choose the fallback
40+
strategy used when none is registered.
4141

4242
::: acopula.register_composition
4343

‎docs/index.md‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,13 @@ forward pass via Taylor-mode AD, scaling polynomially in the dimension.
1111

1212
## Install
1313

14-
Install from source with [uv](https://docs.astral.sh/uv/):
14+
```bash
15+
pip install git+https://github.com/thisiscam/acopula
16+
```
17+
18+
acopula depends on a patched [`oryx`](https://github.com/jax-ml/oryx) build
19+
(pulled from git during install) because stock `oryx` is incompatible with
20+
jax 0.8. For development, clone and use [uv](https://docs.astral.sh/uv/):
1521

1622
```bash
1723
git clone https://github.com/thisiscam/acopula
@@ -20,12 +26,9 @@ uv sync # add --extra examples for the plotting examples
2026
uv run python examples/01_quickstart.py
2127
```
2228

23-
uv is required (for now): `acopula` depends on a patched
24-
[`oryx`](https://github.com/jax-ml/oryx) build pinned via `[tool.uv.sources]`,
25-
and plain `pip install` (including `pip install git+…`) ignores that pin, pulls
26-
the stock PyPI `oryx` (incompatible with jax 0.8), and fails at import. A PyPI
27-
`pip install acopula` is **not available yet** — it is gated on an upstream
28-
`oryx` release compatible with jax 0.8.
29+
A PyPI `pip install acopula` is **not available yet**: the git-pinned `oryx`
30+
dependency can't be uploaded to PyPI, so it is gated on an upstream `oryx`
31+
release compatible with jax 0.8.
2932

3033
`acopula` pins `jax>=0.8,<0.9` because it relies on JAX-internal APIs in the
3134
[jet-array](https://github.com/thisiscam/jet-array) backend.

‎docs/javascripts/mathjax.js‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// MathJax config for Material for MkDocs + pymdownx.arithmatex (generic mode).
2+
window.MathJax = {
3+
tex: {
4+
inlineMath: [["\\(", "\\)"]],
5+
displayMath: [["\\[", "\\]"]],
6+
processEscapes: true,
7+
processEnvironments: true,
8+
},
9+
options: {
10+
ignoreHtmlClass: ".*|",
11+
processHtmlClass: "arithmatex",
12+
},
13+
};
14+
15+
// Re-typeset on Material's instant-navigation page loads.
16+
document$.subscribe(() => {
17+
MathJax.startup.output.clearCache();
18+
MathJax.typesetClear();
19+
MathJax.texReset();
20+
MathJax.typesetPromise();
21+
});

‎docs/quickstart.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ print(grad) # ∂ll/∂params, exact
4949
For survival data, swap the leaf distribution and mark censored observations.
5050
Use `float64` distribution parameters since `acopula` enables
5151
`jax_enable_x64` at import. Pass `survival=True` so leaf inversion uses
52-
`F^{-1}(1 - u)`.
52+
$F^{-1}(1 - u)$.
5353

5454
```python
5555
def survival_model(params, obs):

‎mkdocs.yml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,15 @@ markdown_extensions:
3434
- admonition
3535
- pymdownx.highlight
3636
- pymdownx.superfences
37+
- pymdownx.arithmatex: # $...$ / $$...$$ rendered by MathJax
38+
generic: true
3739
- toc:
3840
permalink: true
3941

42+
extra_javascript:
43+
- javascripts/mathjax.js
44+
- https://unpkg.com/mathjax@3/es5/tex-mml-chtml.js
45+
4046
plugins:
4147
- search
4248
- mkdocstrings:

‎pyproject.toml‎

Lines changed: 15 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,17 @@ classifiers = [
3232
]
3333
dependencies = [
3434
"jax>=0.8,<0.9",
35-
"jet-array==0.1.0a1",
35+
"jet-array==0.1.0a2",
3636
"jaxopt>=0.8",
3737
"optimistix>=0.0.6",
3838
"quadax>=0.2.9",
3939
"equinox>=0.13",
40-
"oryx>=0.2.9",
40+
# Direct ref to the patched fork (jax-0.8 import fixes + copy_p ILDJ +
41+
# tfp-nightly dep). pip and uv both honor this, so `pip install
42+
# git+<acopula>` works. NOTE: a git URL here makes the package
43+
# un-uploadable to PyPI; swap back to `oryx>=<ver>` once a jax-0.8
44+
# compatible oryx is published.
45+
"oryx @ git+https://github.com/thisiscam/oryx.git@copulaad_support",
4146
"tfp-nightly==0.26.0.dev20251116",
4247
"scipy>=1.11",
4348
"sympy>=1.12",
@@ -58,7 +63,7 @@ examples = [
5863
gpu = [
5964
"jax[cuda12]>=0.8,<0.9; sys_platform == 'linux' and platform_machine == 'x86_64'",
6065
]
61-
test = ["pytest>=7", "mpmath>=1.3"]
66+
test = ["pytest>=7", "mpmath>=1.3", "pytest-xdist>=3"]
6267
docs = [
6368
"mkdocs-material>=9.5",
6469
"mkdocstrings[python]>=0.26",
@@ -69,41 +74,20 @@ docs = [
6974
required-environments = [
7075
"sys_platform == 'linux' and platform_machine == 'x86_64'",
7176
]
72-
# oryx pulls in stable `tensorflow-probability`, which collides on disk with
73-
# `tfp-nightly` (both write into the `tensorflow_probability/` namespace).
74-
# Depending on uv's install order, stable's broken `ops.py` (referencing the
75-
# removed `jax.interpreters.xla.pytype_aval_mappings`) wins and crashes at
76-
# import. Drop stable tfp from resolution; tfp-nightly satisfies every oryx
77-
# import path.
78-
override-dependencies = [
79-
"tensorflow-probability ; sys_platform == 'never'",
80-
]
81-
# Pin tfp-nightly to the version that works with jax 0.8.x. Newer nightlies
82-
# remove `jax.interpreters.xla.pytype_aval_mappings` and crash at import.
83-
constraint-dependencies = [
84-
"tfp-nightly==0.26.0.dev20251116",
85-
]
8677

87-
# PyPI's oryx 0.2.9 imports `from jax import util`, which jax 0.8.x removed.
88-
# The `copulaad_support` branch of the patched fork uses `from jax._src import
89-
# util` and ships the lax.copy_p ILDJ shim acopula expects. This source
90-
# override applies only under uv; the published `oryx>=0.2.9` PyPI dep stays
91-
# in `dependencies` so a future plain `pip install acopula` works once the
92-
# upstream fix lands.
93-
[tool.uv.sources]
94-
oryx = { git = "https://github.com/thisiscam/oryx.git", branch = "copulaad_support" }
95-
# PyPI's jet-array==0.1.0a1 predates the log-space Taylor expansion
96-
# (sign, log|c|) representation that `bell.py`'s `log_jet=True` path
97-
# uses, and also predates the log_add NaN-gradient shift-guard at
98-
# structural zeros. Pin to the public fork until a new wheel is
99-
# released.
100-
jet-array = { git = "https://github.com/thisiscam/jet-array.git", branch = "main" }
78+
[tool.hatch.metadata]
79+
# acopula depends on the oryx fork via a git direct reference (see
80+
# [project.dependencies]); hatchling requires opting in to build it.
81+
allow-direct-references = true
10182

10283
[tool.hatch.build.targets.wheel]
10384
packages = ["acopula"]
10485

10586
[tool.pytest.ini_options]
10687
testpaths = ["tests"]
88+
markers = [
89+
"slow: heavy test (deep nesting / e2e sampling / high-d). Deselected in the PR gate via -m 'not slow'; run in the nightly full-suite job.",
90+
]
10791

10892
[build-system]
10993
requires = ["hatchling"]

0 commit comments

Comments
 (0)