Skip to content

Commit f840b1a

Browse files
thisiscamclaude
andcommitted
Drop @defmodel decorator; plain functions work directly
@defmodel was a no-op marker that wrapped a (params, u) -> Node function in a ModelFn container which compile_model immediately unwrapped via _resolve_model_fn. compile_model already accepts plain functions identically, so the decorator added an import and a layer of ceremony with zero runtime effect. Remove ModelFn, defmodel, and _resolve_model_fn from compile.py; narrow compile_model's signature to plain Callable; drop the exports from __init__.py; update the migration comment in core.py; update README quickstart to use compile_model directly (the old set_params/log_likelihood example was bit-rotted to the removed Model class anyway); migrate tests and examples to the plain-function form. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent fa7de8a commit f840b1a

13 files changed

Lines changed: 32 additions & 108 deletions

‎README.md‎

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,14 @@ Uniform leaves, and gradient-based MLE in twenty lines:
3434
import jax
3535
import jax.numpy as jnp
3636
from tensorflow_probability.substrates import jax as tfp
37-
from acopula import copula, defmodel, marginal
37+
from acopula import compile_model, copula, marginal
3838

3939
@copula
4040
class Frank:
4141
theta: float
4242
def generator(self, u):
4343
return -jnp.log1p(jnp.expm1(-self.theta) * jnp.exp(-u)) / self.theta
4444

45-
@defmodel
4645
def model(params, obs):
4746
outer = Frank(params[0])
4847
inner = Frank(params[1])
@@ -54,15 +53,12 @@ def model(params, obs):
5453
obs = jnp.array([[0.3, 0.7],
5554
[0.4, 0.8]])
5655

57-
model.set_params(jnp.array([2.0, 5.0]))
58-
print(model.log_likelihood(obs)) # scalar log-likelihood
56+
cm = compile_model(model, template=jnp.array([1.0, 1.0]))
57+
params = jnp.array([2.0, 5.0])
58+
print(cm.eval(obs, params)) # scalar log-likelihood
5959

60-
def neg_ll(params, obs):
61-
model.set_params(params)
62-
return -model.log_likelihood(obs)
63-
64-
grad = jax.grad(neg_ll)(jnp.array([2.0, 5.0]), obs)
65-
print(grad) # ∂(-ll)/∂params, exact
60+
grad = jax.grad(cm.eval, argnums=1)(obs, params)
61+
print(grad) # ∂ll/∂params, exact
6662
```
6763

6864
### With Weibull marginals and right-censoring
@@ -72,7 +68,6 @@ Use `float64` for the distribution parameters since `acopula` enables
7268
`jax_enable_x64` at import time.
7369

7470
```python
75-
@defmodel
7671
def survival_model(params, obs):
7772
outer = Frank(params[0])
7873
inner = Frank(params[1])
@@ -86,8 +81,9 @@ def survival_model(params, obs):
8681

8782
`@copula` registers the parameters declared as type-annotated class
8883
attributes and derives the inverse generator symbolically via `oryx`.
89-
`@defmodel` traces the function into a copula tree, flattens parameters
90-
into a single array, and exposes `log_likelihood`, all `jit`/`grad`-compatible.
84+
`compile_model` traces the model function into a copula tree, flattens
85+
parameters into a single array, and returns a `CompiledModel` exposing
86+
a jit'd log-likelihood that is `jax.grad`/`jax.vmap`-compatible.
9187
`marginal` pairs each leaf with a distribution and an optional
9288
per-observation censoring flag.
9389

‎acopula/__init__.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
from . import _oryx_patch # noqa: F401 — registers oryx ILDJ rule for lax.copy_p
22
from .core import Copula, copula, marginal
3-
from .compile import compile_model, defmodel, CompiledModel, ModelFn
3+
from .compile import compile_model, CompiledModel
44
from .compose import register_composition, set_composition_fallback
55
from .bell import set_stable_log
66
from ._compile_cache import set_compile_cache_dir
77

88
__all__ = [
99
"Copula", "copula", "marginal",
10-
"compile_model", "defmodel", "CompiledModel", "ModelFn",
10+
"compile_model", "CompiledModel",
1111
"register_composition", "set_composition_fallback",
1212
"set_stable_log",
1313
"set_compile_cache_dir",

‎acopula/compile.py‎

Lines changed: 9 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,14 @@
1616
1717
Typical usage::
1818
19-
from acopula import compile_model, defmodel, marginal, copula
19+
from acopula import compile_model, marginal, copula
2020
2121
@copula
2222
class Clayton:
2323
theta: float
2424
def generator(self, t):
2525
return jnp.power(1.0 + t, -1.0 / self.theta)
2626
27-
@defmodel
2827
def model(p, u):
2928
return Clayton(p["theta"])([
3029
marginal(Uniform(), obs=u[i]) for i in range(5)
@@ -43,48 +42,14 @@ def model(p, u):
4342
from __future__ import annotations
4443

4544
from dataclasses import dataclass
46-
from typing import Any, Callable, Optional, Tuple, Union
45+
from typing import Any, Callable, Optional, Tuple
4746

4847
import jax
4948
import jax.numpy as jnp
5049
from jax import random as jrandom
5150
from oryx import core as oryx_core
5251

5352

54-
@dataclass(frozen=True)
55-
class ModelFn:
56-
"""Marker wrapper produced by ``@defmodel``.
57-
58-
Holding the user function in a typed container lets ``compile_model``
59-
distinguish "function defined as a copula model" from arbitrary
60-
callables, while still supporting plain function passing for users
61-
who skip the decorator.
62-
"""
63-
64-
fn: Callable
65-
66-
def __call__(self, params, u):
67-
return self.fn(params, u)
68-
69-
70-
def defmodel(fn: Callable) -> ModelFn:
71-
"""Mark a function as a copula composition model.
72-
73-
Decorated functions take ``(params, u)`` and must return a Node
74-
produced by an outer copula call (see the package README for the
75-
composition syntax). The decorator itself does no work besides
76-
wrapping; the actual graph construction happens inside
77-
:func:`compile_model`.
78-
"""
79-
return ModelFn(fn=fn)
80-
81-
82-
def _resolve_model_fn(model_fn: Union[Callable, ModelFn]) -> Callable:
83-
if isinstance(model_fn, ModelFn):
84-
return model_fn.fn
85-
return model_fn
86-
87-
8853
def _harvest_graph_and_params(
8954
fn: Callable, template: Any
9055
) -> Tuple[Any, dict]:
@@ -112,7 +77,7 @@ def wrapped_fn(p):
11277

11378
if not isinstance(graph, Node):
11479
raise TypeError(
115-
"@defmodel function must return a Copula call producing a Node"
80+
"Model function must return a Copula call producing a Node"
11681
)
11782

11883
leaves_in_order = _collect_leaves_in_order(graph)
@@ -356,7 +321,7 @@ def visualize(
356321

357322

358323
def compile_model(
359-
model_fn: Union[Callable, ModelFn],
324+
model_fn: Callable,
360325
*,
361326
template: Any,
362327
method: str = "auto",
@@ -381,8 +346,8 @@ def compile_model(
381346
invalidating cache) is avoided by construction.
382347
383348
Args:
384-
model_fn: A function ``(params, u) -> Node`` (typically wrapped
385-
with ``@defmodel``). Plain functions also work.
349+
model_fn: A function ``(params, u) -> Node`` returning a Copula
350+
call that produces the structural graph.
386351
template: A parameter pytree whose structure matches what
387352
``model_fn`` expects. Values are used only to drive oryx's
388353
tracer; pass any sensible defaults (e.g.
@@ -407,10 +372,8 @@ def compile_model(
407372
A :class:`CompiledModel` holding the frozen graph, flattener,
408373
and jit'd log-likelihood.
409374
"""
410-
fn = _resolve_model_fn(model_fn)
411-
412-
graph, _harvested, obs_shape = _harvest_graph_and_params(fn, template)
413-
flatten, param_shape = _make_flatten(fn, template)
375+
graph, _harvested, obs_shape = _harvest_graph_and_params(model_fn, template)
376+
flatten, param_shape = _make_flatten(model_fn, template)
414377

415378
raw_ll = _build_ll_fn(
416379
graph,
@@ -428,7 +391,7 @@ def compile_model(
428391
param_shape=param_shape,
429392
obs_shape=obs_shape,
430393
with_censored_mask=with_censored_mask,
431-
_model_fn=fn,
394+
_model_fn=model_fn,
432395
_flatten_fn=flatten,
433396
_ll_fn_jit=ll_fn_jit,
434397
)

‎acopula/core.py‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -595,9 +595,8 @@ def _reconstruct_obs_from_marginals(
595595
#
596596
# Migrate Model() / set_params() / make_ll_fn() callers to:
597597
#
598-
# from acopula import compile_model, defmodel
598+
# from acopula import compile_model
599599
#
600-
# @defmodel
601600
# def m(p, u): ...
602601
#
603602
# cm = compile_model(m, template={'theta': 1.0}, method='bell')

‎examples/sample_and_plot_pairs.py‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Generate samples using vmap and plot pairwise distributions."""
22

3-
from acopula import copula, defmodel, marginal
3+
from acopula import copula, marginal
44
import jax
55
import jax.numpy as jnp
66
import jax.random as jrandom
@@ -25,7 +25,6 @@ def generator(self, u: jax.Array) -> jax.Array:
2525
return -jnp.log1p(jnp.expm1(-self.theta) * jnp.exp(-u)) / self.theta
2626

2727

28-
@defmodel
2928
def model(params, obs):
3029
c1 = Frank(params[0])
3130
c2 = Frank(params[1])

‎examples/two_level.py‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from acopula import Copula, defmodel
1+
from acopula import Copula
22
import jax
33
import jax.numpy as jnp
44

@@ -17,7 +17,6 @@ def generator(self, u: jax.Array) -> jax.Array:
1717
return -jax.log(1 - (jax.exp(-self.theta * u) - 1) / (jax.exp(-self.theta) - 1))
1818

1919

20-
@defmodel
2120
def model(params, u):
2221
f = Frank(params[0])
2322
c = Clayton(params[1])
@@ -30,7 +29,7 @@ def model(params, u):
3029
jax.random.PRNGKey(0), 100
3130
) # this will generate 100 samples from the model
3231
# under the hood
33-
# defmodel calls the user defined `model` and builds a complete graph of the model
32+
# compile_model traces the user-defined `model` and builds a complete graph
3433
# then, we need to find a "plan" that traverses the graph
3534
# In particular, a plan is a sequence of stages, where each stage is a set of nodes of the same family and all ancestor families.
3635
# For each stage, all the nodes that are inputs to the current staged are already computed and thus available
@@ -49,7 +48,7 @@ def model(params, u):
4948
jnp.ones((10, 20))
5049
) # this will compute the log likelihood of the model
5150
# under the hood
52-
# defmodel calls the user defined `model` and builds a complete graph of
51+
# compile_model traces the user-defined `model` and builds a complete graph
5352
# if the graph is a single layer tree (there's only one node of copula),
5453
# then the log likelihood is simply found by finding the d^th order mixed derivative of the outer generator, multiplied by the product of the derivatives of the inner generators.
5554
# if the graph is a multi-layer tree,

‎examples/visualize_sampling.py‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Example showing step-by-step visualization of sampling computation."""
22

3-
from acopula import copula, defmodel
3+
from acopula import copula
44
import jax
55
import jax.numpy as jnp
66

@@ -21,7 +21,6 @@ def generator(self, u: jax.Array) -> jax.Array:
2121
return -jnp.log1p(jnp.expm1(-self.theta) * jnp.exp(-u)) / self.theta
2222

2323

24-
@defmodel
2524
def model(params, u):
2625
f = Clayton(params[0])
2726
c = Clayton(params[1])

‎examples/visualize_tree.py‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from acopula import Copula, defmodel
1+
from acopula import Copula
22
import jax
33
import jax.numpy as jnp
44

@@ -25,7 +25,6 @@ def generator(self, u: jax.Array) -> jax.Array:
2525
)
2626

2727

28-
@defmodel
2928
def model(params, u):
3029
f = Frank(params[0])
3130
c = Clayton(params[1])

‎tests/test_3level_quick.py‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
jax.config.update("jax_enable_x64", True)
1212

13-
from acopula import compile_model, copula, defmodel, marginal
13+
from acopula import compile_model, copula, marginal
1414

1515
@copula
1616
class Clayton:
@@ -44,7 +44,6 @@ def main():
4444
n_groups, n_sectors, n_leaves = 2, 2, 2
4545
d = n_groups * n_sectors * n_leaves
4646

47-
@defmodel
4847
def model(params, obs):
4948
root = Clayton(params[0])
5049
groups = []

0 commit comments

Comments
 (0)