ablanx 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ablanx-0.1.0/LICENSE ADDED
@@ -0,0 +1,33 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2021, Tobias Hegelund Olsen
4
+ Copyright (c) 2026, Fabricagen
5
+
6
+ This software is a JAX/Flax port of AbLang2 (Oxford Protein Informatics Group). The
7
+ original AbLang2 model, weights, and copyright are the work of the AbLang2 authors and
8
+ are retained above. See ATTRIBUTION.md for details.
9
+
10
+ Redistribution and use in source and binary forms, with or without
11
+ modification, are permitted provided that the following conditions are met:
12
+
13
+ 1. Redistributions of source code must retain the above copyright notice, this
14
+ list of conditions and the following disclaimer.
15
+
16
+ 2. Redistributions in binary form must reproduce the above copyright notice,
17
+ this list of conditions and the following disclaimer in the documentation
18
+ and/or other materials provided with the distribution.
19
+
20
+ 3. Neither the name of the copyright holder nor the names of its
21
+ contributors may be used to endorse or promote products derived from
22
+ this software without specific prior written permission.
23
+
24
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
25
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
26
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
27
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
28
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
29
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
30
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
31
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
32
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
33
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
ablanx-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,159 @@
1
+ Metadata-Version: 2.4
2
+ Name: ablanx
3
+ Version: 0.1.0
4
+ Summary: JAX/Flax port of AbLang2's AbRep encoder: antibody language-model embeddings and per-block attention.
5
+ Author: Fabricagen
6
+ License: BSD-3-Clause
7
+ Project-URL: Homepage, https://github.com/fabricagen/ablanx
8
+ Project-URL: Repository, https://github.com/fabricagen/ablanx
9
+ Keywords: jax,flax,antibody,protein-language-model,ablang,ablang2
10
+ Classifier: License :: OSI Approved :: BSD License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: jax>=0.4.20
18
+ Requires-Dist: jaxlib>=0.4.20
19
+ Requires-Dist: flax>=0.7.5
20
+ Requires-Dist: numpy>=1.24
21
+ Provides-Extra: reference
22
+ Requires-Dist: ablang2; extra == "reference"
23
+ Requires-Dist: torch; extra == "reference"
24
+ Dynamic: license-file
25
+
26
+ # ablanx
27
+
28
+ ablanx is a faithful JAX/Flax port of the AbLang2 AbRep encoder. It loads the original AbLang2
29
+ weights unchanged and returns per-residue embeddings and per-block attention in a single JAX forward
30
+ pass, so an antibody sequence prior can sit inside a larger JAX/Flax model.
31
+
32
+ AbLang2 is the work of the Oxford Protein Informatics Group (OPIG); this repository is a port, not a
33
+ new model. Provenance and citation are in [ATTRIBUTION.md](ATTRIBUTION.md) and
34
+ [CITATION.cff](CITATION.cff).
35
+
36
+ ## Why use it
37
+
38
+ - Drop-in AbLang2 embeddings inside JAX/Flax workflows.
39
+ - No PyTorch dependency at inference (weights load from a plain npz).
40
+ - Pure JAX forward: `jit` / `vmap` / `grad`-compatible and composable in a larger model.
41
+ - Per-block attention returned directly from the same call.
42
+ - Numerically verified against the reference implementation (max abs 3.9e-6 across a 30-Fv panel).
43
+
44
+ ## Quickstart
45
+
46
+ Install:
47
+
48
+ pip install git+https://github.com/fabricagen/ablanx
49
+ # from a clone: pip install .
50
+ # imports as `ablang_jax`; PyPI (pip install ablanx) is planned.
51
+
52
+ Embeddings:
53
+
54
+ import numpy as np, jax.numpy as jnp
55
+ from ablang_jax import Ablanx, load_ablanx_params
56
+
57
+ model = Ablanx()
58
+ params = {"params": load_ablanx_params(dict(np.load("ablang2_weights.npz")))} # AbRep weights
59
+ tokens = jnp.array([[...]], dtype=jnp.int32) # AbLang2 token ids, shape [B, L]
60
+ mask = jnp.ones_like(tokens, dtype=jnp.float32) # 1 = keep, 0 = pad
61
+ hidden, attentions = model.apply(params, tokens, mask, return_attn=True)
62
+ # hidden: [B, L, 480] per-residue embeddings
63
+ # attentions: [12, B, 20, L, L] per-block attention maps
64
+
65
+ See [Weights](#weights) for the npz.
66
+
67
+ ## What it is
68
+
69
+ AbLang2 is an antibody-specific protein language model. Its encoder (AbRep) is a 12-block pre-norm
70
+ transformer: hidden 480, 20 heads, SwiGLU feed-forward, rotary position embeddings, vocab 26,
71
+ trained on paired antibody sequences. `ablanx` re-expresses AbRep in Flax so the original
72
+ BSD-3-Clause weights load without change, returning per-residue hidden states and per-block attention
73
+ maps in one forward pass.
74
+
75
+ ## Differentiability
76
+
77
+ The forward is a pure JAX function, so it composes inside a larger model and runs under `jit`,
78
+ `vmap`, and `grad`. Useful gradients flow with respect to:
79
+
80
+ - the model parameters (`params`), and
81
+ - any continuous input substituted for the token lookup, for example a soft or one-hot embedding
82
+ passed in place of integer ids.
83
+
84
+ The public interface takes integer `tokens`, which are discrete and do not themselves admit
85
+ gradients; to optimize a sequence through the encoder, feed a continuous representation rather than
86
+ token ids. In the seam folder, ablanx runs frozen as a fixed sequence prior.
87
+
88
+ ## What works today
89
+
90
+ - The Flax AbRep encoder, `ablang_jax.model.Ablanx`.
91
+ - The PyTorch-to-Flax weight key mapping, `ablang_jax.model.load_ablanx_params`, which maps an
92
+ AbLang2 AbRep `state_dict` (as numpy arrays) into the Flax parameter tree.
93
+ - A weight exporter, `export_weights.py`, that writes the AbRep weight npz from the reference model.
94
+ - A precompute script, `ablang_jax.precompute`, that runs the reference PyTorch AbLang2 over a set of
95
+ antibody records and stores per-residue embeddings.
96
+
97
+ ## Validation
98
+
99
+ The JAX forward reproduces reference PyTorch AbLang2 to float32 precision. Across a 30-Fv panel (25
100
+ paired VH+VL and 5 VH-only, drawn from public PDB structures), per-residue embeddings match the
101
+ reference with maximum absolute difference 3.9e-6 (median 2.4e-6 per Fv) and cosine above 0.999999,
102
+ using the same weights. The weights are byte-identical to the reference AbRep state_dict (207 tensors,
103
+ exact match). Reproduce with `test_agreement.py`.
104
+
105
+ ![Per-residue embedding agreement across 30 antibody Fvs: identity scatter (cosine 1.000000) and absolute-difference distribution (max 3.9e-6).](agreement.png)
106
+
107
+ ## Weights
108
+
109
+ The weights are the original AbLang2 AbRep weights, unmodified (207 tensors).
110
+
111
+ - Export from the reference model (works today, no release needed):
112
+ `pip install ablang2 torch && python export_weights.py` writes `ablang2_weights.npz`, verified
113
+ byte-identical to the reference.
114
+ - A torch-free `ablang2_weights.npz` is also attached to the GitHub release once one is tagged.
115
+
116
+ Point `ABLANG_WEIGHTS` at the npz, or pass `dict(np.load(...))` to `load_ablanx_params`.
117
+
118
+ ## Not included
119
+
120
+ - Only the AbRep encoder (embeddings and attention) is ported. The amino-acid likelihood head used
121
+ for the pseudo-log-likelihood (sequence naturalness) is not part of this encoder port; use the
122
+ reference AbLang2 for likelihoods.
123
+
124
+ ## Precompute
125
+
126
+ Precompute embeddings for a set of records with the reference PyTorch `ablang2`:
127
+
128
+ export ABLANG_JAX_DATA=/path/to/records
129
+ python -m ablang_jax.precompute --data $ABLANG_JAX_DATA
130
+
131
+ Input records are npz shards named `{train,test}_*.npz`; see `ablang_jax/precompute.py` for the
132
+ expected per-record fields. Outputs default to `./out/` when paths are not set.
133
+
134
+ ## Tests
135
+
136
+ python test_shapes.py # forward, weight-key mapping, masking
137
+ ABLANG_WEIGHTS=ablang2_weights.npz python test_agreement.py # agreement vs reference (needs weights)
138
+
139
+ `test_shapes.py` needs no weights. `test_agreement.py` loads the committed golden panel
140
+ (`golden_ablang2_panel.npz`, reference embeddings for 30 Fvs) and checks the JAX forward matches every
141
+ one; it skips under pytest if `ABLANG_WEIGHTS` is unset.
142
+
143
+ ## Attribution
144
+
145
+ - Original model, training, and weights: AbLang2, Oxford Protein Informatics Group.
146
+ https://github.com/oxpig/AbLang2 (BSD-3-Clause, Copyright (c) 2021, Tobias Hegelund Olsen).
147
+ - Paper: Olsen, Moal, Deane, "Addressing the antibody germline bias and its effect on language models
148
+ for improved antibody design", bioRxiv 2024, doi:10.1101/2024.02.02.578678.
149
+ - This port: Fabricagen.
150
+
151
+ ablanx is the language-model component of the **seam** bundle, a JAX antibody Fv structure predictor
152
+ that couples jaxfvld with ablanx. For antibody developability screening and repair, see **sift**:
153
+ https://sift.fabricagen.ai (coming soon, ca 08/26)
154
+
155
+ ## License
156
+
157
+ BSD-3-Clause. This port preserves the original AbLang2 copyright (Copyright (c) 2021, Tobias Hegelund
158
+ Olsen) and adds Copyright (c) 2026, Fabricagen for the port. The upstream AbLang2 license was
159
+ confirmed BSD-3-Clause. See `ATTRIBUTION.md`.
ablanx-0.1.0/README.md ADDED
@@ -0,0 +1,134 @@
1
+ # ablanx
2
+
3
+ ablanx is a faithful JAX/Flax port of the AbLang2 AbRep encoder. It loads the original AbLang2
4
+ weights unchanged and returns per-residue embeddings and per-block attention in a single JAX forward
5
+ pass, so an antibody sequence prior can sit inside a larger JAX/Flax model.
6
+
7
+ AbLang2 is the work of the Oxford Protein Informatics Group (OPIG); this repository is a port, not a
8
+ new model. Provenance and citation are in [ATTRIBUTION.md](ATTRIBUTION.md) and
9
+ [CITATION.cff](CITATION.cff).
10
+
11
+ ## Why use it
12
+
13
+ - Drop-in AbLang2 embeddings inside JAX/Flax workflows.
14
+ - No PyTorch dependency at inference (weights load from a plain npz).
15
+ - Pure JAX forward: `jit` / `vmap` / `grad`-compatible and composable in a larger model.
16
+ - Per-block attention returned directly from the same call.
17
+ - Numerically verified against the reference implementation (max abs 3.9e-6 across a 30-Fv panel).
18
+
19
+ ## Quickstart
20
+
21
+ Install:
22
+
23
+ pip install git+https://github.com/fabricagen/ablanx
24
+ # from a clone: pip install .
25
+ # imports as `ablang_jax`; PyPI (pip install ablanx) is planned.
26
+
27
+ Embeddings:
28
+
29
+ import numpy as np, jax.numpy as jnp
30
+ from ablang_jax import Ablanx, load_ablanx_params
31
+
32
+ model = Ablanx()
33
+ params = {"params": load_ablanx_params(dict(np.load("ablang2_weights.npz")))} # AbRep weights
34
+ tokens = jnp.array([[...]], dtype=jnp.int32) # AbLang2 token ids, shape [B, L]
35
+ mask = jnp.ones_like(tokens, dtype=jnp.float32) # 1 = keep, 0 = pad
36
+ hidden, attentions = model.apply(params, tokens, mask, return_attn=True)
37
+ # hidden: [B, L, 480] per-residue embeddings
38
+ # attentions: [12, B, 20, L, L] per-block attention maps
39
+
40
+ See [Weights](#weights) for the npz.
41
+
42
+ ## What it is
43
+
44
+ AbLang2 is an antibody-specific protein language model. Its encoder (AbRep) is a 12-block pre-norm
45
+ transformer: hidden 480, 20 heads, SwiGLU feed-forward, rotary position embeddings, vocab 26,
46
+ trained on paired antibody sequences. `ablanx` re-expresses AbRep in Flax so the original
47
+ BSD-3-Clause weights load without change, returning per-residue hidden states and per-block attention
48
+ maps in one forward pass.
49
+
50
+ ## Differentiability
51
+
52
+ The forward is a pure JAX function, so it composes inside a larger model and runs under `jit`,
53
+ `vmap`, and `grad`. Useful gradients flow with respect to:
54
+
55
+ - the model parameters (`params`), and
56
+ - any continuous input substituted for the token lookup, for example a soft or one-hot embedding
57
+ passed in place of integer ids.
58
+
59
+ The public interface takes integer `tokens`, which are discrete and do not themselves admit
60
+ gradients; to optimize a sequence through the encoder, feed a continuous representation rather than
61
+ token ids. In the seam folder, ablanx runs frozen as a fixed sequence prior.
62
+
63
+ ## What works today
64
+
65
+ - The Flax AbRep encoder, `ablang_jax.model.Ablanx`.
66
+ - The PyTorch-to-Flax weight key mapping, `ablang_jax.model.load_ablanx_params`, which maps an
67
+ AbLang2 AbRep `state_dict` (as numpy arrays) into the Flax parameter tree.
68
+ - A weight exporter, `export_weights.py`, that writes the AbRep weight npz from the reference model.
69
+ - A precompute script, `ablang_jax.precompute`, that runs the reference PyTorch AbLang2 over a set of
70
+ antibody records and stores per-residue embeddings.
71
+
72
+ ## Validation
73
+
74
+ The JAX forward reproduces reference PyTorch AbLang2 to float32 precision. Across a 30-Fv panel (25
75
+ paired VH+VL and 5 VH-only, drawn from public PDB structures), per-residue embeddings match the
76
+ reference with maximum absolute difference 3.9e-6 (median 2.4e-6 per Fv) and cosine above 0.999999,
77
+ using the same weights. The weights are byte-identical to the reference AbRep state_dict (207 tensors,
78
+ exact match). Reproduce with `test_agreement.py`.
79
+
80
+ ![Per-residue embedding agreement across 30 antibody Fvs: identity scatter (cosine 1.000000) and absolute-difference distribution (max 3.9e-6).](agreement.png)
81
+
82
+ ## Weights
83
+
84
+ The weights are the original AbLang2 AbRep weights, unmodified (207 tensors).
85
+
86
+ - Export from the reference model (works today, no release needed):
87
+ `pip install ablang2 torch && python export_weights.py` writes `ablang2_weights.npz`, verified
88
+ byte-identical to the reference.
89
+ - A torch-free `ablang2_weights.npz` is also attached to the GitHub release once one is tagged.
90
+
91
+ Point `ABLANG_WEIGHTS` at the npz, or pass `dict(np.load(...))` to `load_ablanx_params`.
92
+
93
+ ## Not included
94
+
95
+ - Only the AbRep encoder (embeddings and attention) is ported. The amino-acid likelihood head used
96
+ for the pseudo-log-likelihood (sequence naturalness) is not part of this encoder port; use the
97
+ reference AbLang2 for likelihoods.
98
+
99
+ ## Precompute
100
+
101
+ Precompute embeddings for a set of records with the reference PyTorch `ablang2`:
102
+
103
+ export ABLANG_JAX_DATA=/path/to/records
104
+ python -m ablang_jax.precompute --data $ABLANG_JAX_DATA
105
+
106
+ Input records are npz shards named `{train,test}_*.npz`; see `ablang_jax/precompute.py` for the
107
+ expected per-record fields. Outputs default to `./out/` when paths are not set.
108
+
109
+ ## Tests
110
+
111
+ python test_shapes.py # forward, weight-key mapping, masking
112
+ ABLANG_WEIGHTS=ablang2_weights.npz python test_agreement.py # agreement vs reference (needs weights)
113
+
114
+ `test_shapes.py` needs no weights. `test_agreement.py` loads the committed golden panel
115
+ (`golden_ablang2_panel.npz`, reference embeddings for 30 Fvs) and checks the JAX forward matches every
116
+ one; it skips under pytest if `ABLANG_WEIGHTS` is unset.
117
+
118
+ ## Attribution
119
+
120
+ - Original model, training, and weights: AbLang2, Oxford Protein Informatics Group.
121
+ https://github.com/oxpig/AbLang2 (BSD-3-Clause, Copyright (c) 2021, Tobias Hegelund Olsen).
122
+ - Paper: Olsen, Moal, Deane, "Addressing the antibody germline bias and its effect on language models
123
+ for improved antibody design", bioRxiv 2024, doi:10.1101/2024.02.02.578678.
124
+ - This port: Fabricagen.
125
+
126
+ ablanx is the language-model component of the **seam** bundle, a JAX antibody Fv structure predictor
127
+ that couples jaxfvld with ablanx. For antibody developability screening and repair, see **sift**:
128
+ https://sift.fabricagen.ai (coming soon, ca 08/26)
129
+
130
+ ## License
131
+
132
+ BSD-3-Clause. This port preserves the original AbLang2 copyright (Copyright (c) 2021, Tobias Hegelund
133
+ Olsen) and adds Copyright (c) 2026, Fabricagen for the port. The upstream AbLang2 license was
134
+ confirmed BSD-3-Clause. See `ATTRIBUTION.md`.
@@ -0,0 +1,32 @@
1
+ """ablang_jax - a JAX/Flax port of AbLang2 (OPIG).
2
+
3
+ This package re-implements AbLang2's AbRep encoder in Flax and provides the state_dict key
4
+ mapping needed to load the original BSD-3-Clause PyTorch weights unchanged. The AbLang2 model
5
+ and weights are the work of the AbLang2 authors (OPIG); this is a port. See ATTRIBUTION.md and
6
+ https://github.com/oxpig/AbLang2.
7
+ """
8
+ from .model import (
9
+ HEAD_DIM,
10
+ HID,
11
+ LN_EPS,
12
+ N_BLOCK,
13
+ N_HEAD,
14
+ VOCAB,
15
+ Ablanx,
16
+ Block,
17
+ load_ablanx_params,
18
+ )
19
+
20
+ __all__ = [
21
+ "Ablanx",
22
+ "Block",
23
+ "load_ablanx_params",
24
+ "VOCAB",
25
+ "HID",
26
+ "N_HEAD",
27
+ "N_BLOCK",
28
+ "HEAD_DIM",
29
+ "LN_EPS",
30
+ ]
31
+
32
+ __version__ = "0.1.0"
@@ -0,0 +1,109 @@
1
+ """Flax reimplementation of AbLang2's AbRep encoder.
2
+
3
+ A 12-block pre-norm transformer (hidden 480, 20 heads, SwiGLU feed-forward, rotary positions,
4
+ vocab 26) that reproduces AbLang2's AbRep so the original BSD-3-Clause PyTorch weights load
5
+ unchanged. A single forward returns per-residue hidden states and per-block attention maps.
6
+
7
+ PyTorch Linear weights [out, in] map to Flax Dense kernels [in, out] by transpose; see
8
+ load_ablanx_params for the full state_dict key mapping. Outputs match reference PyTorch AbLang2 to under
9
+ 4e-6 across a 30-Fv panel (test_agreement.py).
10
+
11
+ AbLang2 is the work of the Oxford Protein Informatics Group; this is a port. See ATTRIBUTION.md and
12
+ https://github.com/oxpig/AbLang2.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import flax.linen as nn
17
+ import jax
18
+ import jax.numpy as jnp
19
+
20
+ VOCAB = 26
21
+ HID = 480
22
+ N_HEAD = 20
23
+ N_BLOCK = 12
24
+ HEAD_DIM = HID // N_HEAD # 24
25
+ LN_EPS = 1e-12
26
+
27
+
28
+ def _rope_freqs(head_dim, base=10000.0):
29
+ # inverse frequencies over half the head dim (matches AbLang2's rotary_emb.freqs)
30
+ half = head_dim // 2
31
+ return 1.0 / (base ** (jnp.arange(0, half, dtype=jnp.float32) * 2.0 / head_dim))
32
+
33
+
34
+ def _apply_rope(x, freqs):
35
+ """Rotary embedding on x [B, L, H, D], interleaved convention (as AbLang2 uses)."""
36
+ L = x.shape[1]
37
+ pos = jnp.arange(L, dtype=jnp.float32)
38
+ ang = pos[:, None] * freqs[None, :]
39
+ ang = jnp.repeat(ang, 2, axis=-1)
40
+ cos = jnp.cos(ang)[None, :, None, :]
41
+ sin = jnp.sin(ang)[None, :, None, :]
42
+ x_even = x[..., 0::2]; x_odd = x[..., 1::2]
43
+ rot = jnp.stack([-x_odd, x_even], axis=-1).reshape(x.shape)
44
+ return x * cos + rot * sin
45
+
46
+
47
+ class Block(nn.Module):
48
+ @nn.compact
49
+ def __call__(self, x, mask, freqs, return_attn=False):
50
+ B, L, _ = x.shape
51
+ h = nn.LayerNorm(epsilon=LN_EPS, name="pre_attn_layer_norm")(x)
52
+ q = nn.Dense(HID, name="q_proj")(h).reshape(B, L, N_HEAD, HEAD_DIM)
53
+ k = nn.Dense(HID, name="k_proj")(h).reshape(B, L, N_HEAD, HEAD_DIM)
54
+ v = nn.Dense(HID, name="v_proj")(h).reshape(B, L, N_HEAD, HEAD_DIM)
55
+ q = _apply_rope(q, freqs); k = _apply_rope(k, freqs)
56
+ # AbLang2's net attention scale is 1/head_dim (q scaled by head_dim^-0.5, then divided by sqrt(head_dim))
57
+ logits = jnp.einsum("blhd,bmhd->bhlm", q, k) / HEAD_DIM
58
+ logits = logits + (1.0 - mask[:, None, None, :]) * -1e9
59
+ attn = jax.nn.softmax(logits, axis=-1)
60
+ o = jnp.einsum("bhlm,bmhd->blhd", attn, v).reshape(B, L, HID)
61
+ o = nn.Dense(HID, name="out_proj")(o)
62
+ x = x + o
63
+ # SwiGLU: split the feed-forward projection into value and gate, gate with silu
64
+ h2 = nn.LayerNorm(epsilon=LN_EPS, name="final_layer_norm")(x)
65
+ up = nn.Dense(3840, name="ffn_in")(h2)
66
+ val, gate = jnp.split(up, 2, axis=-1)
67
+ x = x + nn.Dense(HID, name="ffn_out")(jax.nn.silu(gate) * val)
68
+ return (x, attn) if return_attn else (x, None)
69
+
70
+
71
+ class Ablanx(nn.Module):
72
+ """AbLang2 AbRep encoder. Returns hidden [B, L, 480] and, optionally, attention [N_BLOCK, B, H, L, L]."""
73
+ @nn.compact
74
+ def __call__(self, tokens, mask, return_attn=True):
75
+ freqs = _rope_freqs(HEAD_DIM)
76
+ x = nn.Embed(VOCAB, HID, name="aa_embed_layer")(tokens.astype("int32"))
77
+ attns = []
78
+ for i in range(N_BLOCK):
79
+ x, a = Block(name=f"encoder_blocks_{i}")(x, mask, freqs, return_attn=return_attn)
80
+ if return_attn:
81
+ attns.append(a)
82
+ # final layer norm after all blocks; this is what rescoding returns
83
+ x = nn.LayerNorm(epsilon=LN_EPS, name="layer_norm_after_encoder_blocks")(x)
84
+ return x, (jnp.stack(attns) if return_attn else None)
85
+
86
+
87
+ def load_ablanx_params(w):
88
+ """Map AbLang2 AbRep PyTorch state_dict (dict of numpy arrays, keys 'AbRep....') -> flax param tree.
89
+ PyTorch Linear weight [out,in] -> flax Dense kernel [in,out] (transpose). LayerNorm weight->scale."""
90
+ import numpy as np
91
+ def T(k): return np.asarray(w[k]).T
92
+ def V(k): return np.asarray(w[k])
93
+ p = {"aa_embed_layer": {"embedding": V("AbRep.aa_embed_layer.weight")},
94
+ "layer_norm_after_encoder_blocks": {"scale": V("AbRep.layer_norm_after_encoder_blocks.weight"),
95
+ "bias": V("AbRep.layer_norm_after_encoder_blocks.bias")}}
96
+ for i in range(N_BLOCK):
97
+ b = f"AbRep.encoder_blocks.{i}"
98
+ p[f"encoder_blocks_{i}"] = {
99
+ "pre_attn_layer_norm": {"scale": V(f"{b}.pre_attn_layer_norm.weight"),
100
+ "bias": V(f"{b}.pre_attn_layer_norm.bias")},
101
+ "q_proj": {"kernel": T(f"{b}.multihead_attention.q_proj.weight"), "bias": V(f"{b}.multihead_attention.q_proj.bias")},
102
+ "k_proj": {"kernel": T(f"{b}.multihead_attention.k_proj.weight"), "bias": V(f"{b}.multihead_attention.k_proj.bias")},
103
+ "v_proj": {"kernel": T(f"{b}.multihead_attention.v_proj.weight"), "bias": V(f"{b}.multihead_attention.v_proj.bias")},
104
+ "out_proj": {"kernel": T(f"{b}.multihead_attention.out_proj.weight"), "bias": V(f"{b}.multihead_attention.out_proj.bias")},
105
+ "ffn_in": {"kernel": T(f"{b}.intermediate_layer.0.weight"), "bias": V(f"{b}.intermediate_layer.0.bias")},
106
+ "ffn_out": {"kernel": T(f"{b}.intermediate_layer.2.weight"), "bias": V(f"{b}.intermediate_layer.2.bias")},
107
+ "final_layer_norm": {"scale": V(f"{b}.final_layer_norm.weight"), "bias": V(f"{b}.final_layer_norm.bias")},
108
+ }
109
+ return p
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env python3
2
+ """ablang_jax.precompute - precompute AbLang2 per-residue embeddings for a set of antibody records.
3
+
4
+ Runs the reference PyTorch AbLang2 (`pip install ablang2`) over antibody Fv sequences stored as
5
+ sharded .npz files and writes a per-record `seq_emb` field ([L, 480], VH then VL, residue positions
6
+ only). These embeddings can be used as a fixed antibody sequence prior.
7
+
8
+ This uses the reference PyTorch AbLang2, not the JAX port: it materialises per-residue embeddings
9
+ with the original model for use as a fixed antibody sequence prior.
10
+
11
+ Expected input: one or more npz shards named `{split}_*.npz` (split in {train, test}) under the data
12
+ directory, each an object-array store with per-record fields:
13
+ pdb_id [N] record id
14
+ aa [N][Li] integer amino-acid indices in the order "ARNDCQEGHILKMFPSTWYV"
15
+ chain_id [N][Li] 0 = heavy (VH), 1 = light (VL)
16
+ res_present [N][Li] bool mask of resolved residues
17
+ Each shard is rewritten in place with an added `seq_emb` object field.
18
+
19
+ python -m ablang_jax.precompute --data /path/to/records
20
+ # or set ABLANG_JAX_DATA (default: ./out/records)
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import argparse
25
+ import glob
26
+ import os
27
+
28
+ import ablang2
29
+ import numpy as np
30
+
31
+ _AA_ORDER = "ARNDCQEGHILKMFPSTWYV" # AlphaFold-style restype index order
32
+
33
+ _DEFAULT_OUT = os.environ.get("ABLANG_JAX_OUT", "./out")
34
+ _DEFAULT_DATA = os.environ.get("ABLANG_JAX_DATA", os.path.join(_DEFAULT_OUT, "records"))
35
+
36
+
37
+ def embed_fv(model, vh_seq, vl_seq):
38
+ """[L,480] per-residue AbLang2 embedding, residue positions only (ids 1..20), VH then VL."""
39
+ pair = [[vh_seq, vl_seq]] if vl_seq else [[vh_seq, ""]]
40
+ emb = np.asarray(model(pair, mode="rescoding")[0], np.float32) # [T_tok, 480]
41
+ ids = np.asarray(model.tokenizer(pair, pad=True))[0] # [T_tok]
42
+ keep = (ids >= 1) & (ids <= 20) # 20 standard AAs
43
+ return emb[: len(ids)][keep[: emb.shape[0]]] # [n_residues, 480]
44
+
45
+
46
+ def main():
47
+ ap = argparse.ArgumentParser()
48
+ ap.add_argument("--data", default=_DEFAULT_DATA,
49
+ help="directory of {train,test}_*.npz shards (env: ABLANG_JAX_DATA)")
50
+ ap.add_argument("--limit-shards", type=int, default=0)
51
+ args = ap.parse_args()
52
+ model = ablang2.pretrained()
53
+
54
+ for split in ("train", "test"):
55
+ shards = sorted(glob.glob(os.path.join(args.data, f"{split}_*.npz")))
56
+ if args.limit_shards:
57
+ shards = shards[: args.limit_shards]
58
+ for si, shard in enumerate(shards):
59
+ z = np.load(shard, allow_pickle=True); arrs = {k: z[k] for k in z.files}
60
+ n = len(arrs["pdb_id"])
61
+ embs = []
62
+ for i in range(n):
63
+ aa = np.asarray(arrs["aa"][i]); cid = np.asarray(arrs["chain_id"][i])
64
+ pres = np.asarray(arrs["res_present"][i], bool)
65
+ vh = "".join(_AA_ORDER[a] for a in aa[(cid == 0) & pres])
66
+ vl = "".join(_AA_ORDER[a] for a in aa[(cid == 1) & pres])
67
+ e = embed_fv(model, vh, vl) # [len(vh)+len(vl), 480]
68
+ exp = len(vh) + len(vl)
69
+ if e.shape[0] != exp: # alignment guard
70
+ e = e[:exp] if e.shape[0] > exp else np.pad(e, [(0, exp - e.shape[0]), (0, 0)])
71
+ embs.append(e.astype(np.float32))
72
+ arrs["seq_emb"] = np.array(embs, dtype=object)
73
+ np.savez_compressed(shard, **arrs)
74
+ if si % 5 == 0:
75
+ print(f"{split} shard {si+1}/{len(shards)} emb[0]={embs[0].shape}", flush=True)
76
+ print(f"{split}: DONE ({len(shards)} shards)", flush=True)
77
+
78
+
79
+ if __name__ == "__main__":
80
+ main()
@@ -0,0 +1,159 @@
1
+ Metadata-Version: 2.4
2
+ Name: ablanx
3
+ Version: 0.1.0
4
+ Summary: JAX/Flax port of AbLang2's AbRep encoder: antibody language-model embeddings and per-block attention.
5
+ Author: Fabricagen
6
+ License: BSD-3-Clause
7
+ Project-URL: Homepage, https://github.com/fabricagen/ablanx
8
+ Project-URL: Repository, https://github.com/fabricagen/ablanx
9
+ Keywords: jax,flax,antibody,protein-language-model,ablang,ablang2
10
+ Classifier: License :: OSI Approved :: BSD License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: jax>=0.4.20
18
+ Requires-Dist: jaxlib>=0.4.20
19
+ Requires-Dist: flax>=0.7.5
20
+ Requires-Dist: numpy>=1.24
21
+ Provides-Extra: reference
22
+ Requires-Dist: ablang2; extra == "reference"
23
+ Requires-Dist: torch; extra == "reference"
24
+ Dynamic: license-file
25
+
26
+ # ablanx
27
+
28
+ ablanx is a faithful JAX/Flax port of the AbLang2 AbRep encoder. It loads the original AbLang2
29
+ weights unchanged and returns per-residue embeddings and per-block attention in a single JAX forward
30
+ pass, so an antibody sequence prior can sit inside a larger JAX/Flax model.
31
+
32
+ AbLang2 is the work of the Oxford Protein Informatics Group (OPIG); this repository is a port, not a
33
+ new model. Provenance and citation are in [ATTRIBUTION.md](ATTRIBUTION.md) and
34
+ [CITATION.cff](CITATION.cff).
35
+
36
+ ## Why use it
37
+
38
+ - Drop-in AbLang2 embeddings inside JAX/Flax workflows.
39
+ - No PyTorch dependency at inference (weights load from a plain npz).
40
+ - Pure JAX forward: `jit` / `vmap` / `grad`-compatible and composable in a larger model.
41
+ - Per-block attention returned directly from the same call.
42
+ - Numerically verified against the reference implementation (max abs 3.9e-6 across a 30-Fv panel).
43
+
44
+ ## Quickstart
45
+
46
+ Install:
47
+
48
+ pip install git+https://github.com/fabricagen/ablanx
49
+ # from a clone: pip install .
50
+ # imports as `ablang_jax`; PyPI (pip install ablanx) is planned.
51
+
52
+ Embeddings:
53
+
54
+ import numpy as np, jax.numpy as jnp
55
+ from ablang_jax import Ablanx, load_ablanx_params
56
+
57
+ model = Ablanx()
58
+ params = {"params": load_ablanx_params(dict(np.load("ablang2_weights.npz")))} # AbRep weights
59
+ tokens = jnp.array([[...]], dtype=jnp.int32) # AbLang2 token ids, shape [B, L]
60
+ mask = jnp.ones_like(tokens, dtype=jnp.float32) # 1 = keep, 0 = pad
61
+ hidden, attentions = model.apply(params, tokens, mask, return_attn=True)
62
+ # hidden: [B, L, 480] per-residue embeddings
63
+ # attentions: [12, B, 20, L, L] per-block attention maps
64
+
65
+ See [Weights](#weights) for the npz.
66
+
67
+ ## What it is
68
+
69
+ AbLang2 is an antibody-specific protein language model. Its encoder (AbRep) is a 12-block pre-norm
70
+ transformer: hidden 480, 20 heads, SwiGLU feed-forward, rotary position embeddings, vocab 26,
71
+ trained on paired antibody sequences. `ablanx` re-expresses AbRep in Flax so the original
72
+ BSD-3-Clause weights load without change, returning per-residue hidden states and per-block attention
73
+ maps in one forward pass.
74
+
75
+ ## Differentiability
76
+
77
+ The forward is a pure JAX function, so it composes inside a larger model and runs under `jit`,
78
+ `vmap`, and `grad`. Useful gradients flow with respect to:
79
+
80
+ - the model parameters (`params`), and
81
+ - any continuous input substituted for the token lookup, for example a soft or one-hot embedding
82
+ passed in place of integer ids.
83
+
84
+ The public interface takes integer `tokens`, which are discrete and do not themselves admit
85
+ gradients; to optimize a sequence through the encoder, feed a continuous representation rather than
86
+ token ids. In the seam folder, ablanx runs frozen as a fixed sequence prior.
87
+
88
+ ## What works today
89
+
90
+ - The Flax AbRep encoder, `ablang_jax.model.Ablanx`.
91
+ - The PyTorch-to-Flax weight key mapping, `ablang_jax.model.load_ablanx_params`, which maps an
92
+ AbLang2 AbRep `state_dict` (as numpy arrays) into the Flax parameter tree.
93
+ - A weight exporter, `export_weights.py`, that writes the AbRep weight npz from the reference model.
94
+ - A precompute script, `ablang_jax.precompute`, that runs the reference PyTorch AbLang2 over a set of
95
+ antibody records and stores per-residue embeddings.
96
+
97
+ ## Validation
98
+
99
+ The JAX forward reproduces reference PyTorch AbLang2 to float32 precision. Across a 30-Fv panel (25
100
+ paired VH+VL and 5 VH-only, drawn from public PDB structures), per-residue embeddings match the
101
+ reference with maximum absolute difference 3.9e-6 (median 2.4e-6 per Fv) and cosine above 0.999999,
102
+ using the same weights. The weights are byte-identical to the reference AbRep state_dict (207 tensors,
103
+ exact match). Reproduce with `test_agreement.py`.
104
+
105
+ ![Per-residue embedding agreement across 30 antibody Fvs: identity scatter (cosine 1.000000) and absolute-difference distribution (max 3.9e-6).](agreement.png)
106
+
107
+ ## Weights
108
+
109
+ The weights are the original AbLang2 AbRep weights, unmodified (207 tensors).
110
+
111
+ - Export from the reference model (works today, no release needed):
112
+ `pip install ablang2 torch && python export_weights.py` writes `ablang2_weights.npz`, verified
113
+ byte-identical to the reference.
114
+ - A torch-free `ablang2_weights.npz` is also attached to the GitHub release once one is tagged.
115
+
116
+ Point `ABLANG_WEIGHTS` at the npz, or pass `dict(np.load(...))` to `load_ablanx_params`.
117
+
118
+ ## Not included
119
+
120
+ - Only the AbRep encoder (embeddings and attention) is ported. The amino-acid likelihood head used
121
+ for the pseudo-log-likelihood (sequence naturalness) is not part of this encoder port; use the
122
+ reference AbLang2 for likelihoods.
123
+
124
+ ## Precompute
125
+
126
+ Precompute embeddings for a set of records with the reference PyTorch `ablang2`:
127
+
128
+ export ABLANG_JAX_DATA=/path/to/records
129
+ python -m ablang_jax.precompute --data $ABLANG_JAX_DATA
130
+
131
+ Input records are npz shards named `{train,test}_*.npz`; see `ablang_jax/precompute.py` for the
132
+ expected per-record fields. Outputs default to `./out/` when paths are not set.
133
+
134
+ ## Tests
135
+
136
+ python test_shapes.py # forward, weight-key mapping, masking
137
+ ABLANG_WEIGHTS=ablang2_weights.npz python test_agreement.py # agreement vs reference (needs weights)
138
+
139
+ `test_shapes.py` needs no weights. `test_agreement.py` loads the committed golden panel
140
+ (`golden_ablang2_panel.npz`, reference embeddings for 30 Fvs) and checks the JAX forward matches every
141
+ one; it skips under pytest if `ABLANG_WEIGHTS` is unset.
142
+
143
+ ## Attribution
144
+
145
+ - Original model, training, and weights: AbLang2, Oxford Protein Informatics Group.
146
+ https://github.com/oxpig/AbLang2 (BSD-3-Clause, Copyright (c) 2021, Tobias Hegelund Olsen).
147
+ - Paper: Olsen, Moal, Deane, "Addressing the antibody germline bias and its effect on language models
148
+ for improved antibody design", bioRxiv 2024, doi:10.1101/2024.02.02.578678.
149
+ - This port: Fabricagen.
150
+
151
+ ablanx is the language-model component of the **seam** bundle, a JAX antibody Fv structure predictor
152
+ that couples jaxfvld with ablanx. For antibody developability screening and repair, see **sift**:
153
+ https://sift.fabricagen.ai (coming soon, ca 08/26)
154
+
155
+ ## License
156
+
157
+ BSD-3-Clause. This port preserves the original AbLang2 copyright (Copyright (c) 2021, Tobias Hegelund
158
+ Olsen) and adds Copyright (c) 2026, Fabricagen for the port. The upstream AbLang2 license was
159
+ confirmed BSD-3-Clause. See `ATTRIBUTION.md`.
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ ablang_jax/__init__.py
5
+ ablang_jax/model.py
6
+ ablang_jax/precompute.py
7
+ ablanx.egg-info/PKG-INFO
8
+ ablanx.egg-info/SOURCES.txt
9
+ ablanx.egg-info/dependency_links.txt
10
+ ablanx.egg-info/requires.txt
11
+ ablanx.egg-info/top_level.txt
@@ -0,0 +1,8 @@
1
+ jax>=0.4.20
2
+ jaxlib>=0.4.20
3
+ flax>=0.7.5
4
+ numpy>=1.24
5
+
6
+ [reference]
7
+ ablang2
8
+ torch
@@ -0,0 +1 @@
1
+ ablang_jax
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ablanx"
7
+ version = "0.1.0"
8
+ description = "JAX/Flax port of AbLang2's AbRep encoder: antibody language-model embeddings and per-block attention."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "BSD-3-Clause" }
12
+ authors = [{ name = "Fabricagen" }]
13
+ keywords = ["jax", "flax", "antibody", "protein-language-model", "ablang", "ablang2"]
14
+ classifiers = [
15
+ "License :: OSI Approved :: BSD License",
16
+ "Programming Language :: Python :: 3",
17
+ "Intended Audience :: Science/Research",
18
+ "Topic :: Scientific/Engineering :: Bio-Informatics",
19
+ ]
20
+ dependencies = [
21
+ "jax>=0.4.20",
22
+ "jaxlib>=0.4.20",
23
+ "flax>=0.7.5",
24
+ "numpy>=1.24",
25
+ ]
26
+
27
+ # Reference PyTorch AbLang2, needed only to export weights or run precompute (not for inference).
28
+ [project.optional-dependencies]
29
+ reference = ["ablang2", "torch"]
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/fabricagen/ablanx"
33
+ Repository = "https://github.com/fabricagen/ablanx"
34
+
35
+ [tool.setuptools]
36
+ packages = ["ablang_jax"]
ablanx-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+