eff-len 0.1.0__py3-none-any.whl

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.
eff_len/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ from .div import *
2
+ from .utils import *
eff_len/div.py ADDED
@@ -0,0 +1,262 @@
1
+ """
2
+ """
3
+
4
+ import numpy as np
5
+ from numpy.lib.stride_tricks import as_strided
6
+ from .utils import msa_to_oh
7
+
8
+
9
+ def average_dist(X):
10
+ N, L, k = X.shape
11
+ X = X.reshape(N, -1)
12
+ mat_dist = L - X @ X.T
13
+ avg_dist = mat_dist[np.triu_indices(N, k=1)].mean()
14
+ return avg_dist
15
+
16
+
17
+ def kmers_from_id_matrix(ids, kmer_len):
18
+ ids = np.asarray(ids)
19
+ N, L = ids.shape
20
+
21
+ n_kmers = L - kmer_len + 1
22
+ sN, sL = ids.strides
23
+
24
+ kmers = as_strided(ids, shape=(N, n_kmers, kmer_len), strides=(sN, sL, sL))
25
+ return kmers.copy()
26
+
27
+
28
+ def kmer_entropy(ids, kmer_len):
29
+ kmers = kmers_from_id_matrix(ids, kmer_len) # (N, n_kmers, kmer_len)
30
+ flat = kmers.reshape(-1, kmer_len) # all k-mers in one list
31
+
32
+ unique_kmers, counts = np.unique(flat, axis=0, return_counts=True)
33
+ p = counts / counts.sum()
34
+ H = -np.sum(p * np.log(p + 1e-15)) # numerical safety
35
+ return np.exp(H)
36
+
37
+
38
+ def pos_entropy(X: np.ndarray, pos: bool = False, leff: bool=False, neff: bool = False, eps: float = 1e-12):
39
+ assert len(X.shape) == 3, "MSA must be NxLxk, where N is the number of sequences, L the length, and k is 5 or 21"
40
+ N, L, k = X.shape
41
+ p = X.mean(axis=0) # frequency per symbol
42
+ p = np.clip(p, eps, 1.0) # avoid log(0)
43
+ H = -(p * np.log(p)).sum(axis=-1)
44
+ if neff:
45
+ return np.exp(H.sum())
46
+ elif pos:
47
+ return np.exp(H)
48
+ elif leff:
49
+ return H.sum()/np.log(k)
50
+ else:
51
+ return np.exp(H.mean())
52
+
53
+
54
+ def neff_seq(X: np.ndarray, thres: float = 0.8, wei: bool = False):
55
+ N, L, k = X.shape
56
+ X = X.reshape(N, -1)
57
+ C = X @ X.T / L
58
+
59
+ S = (C > thres).astype(float) # similarity matrix
60
+ counts = S.sum(axis=1)
61
+
62
+ w = 1.0 / counts
63
+
64
+ if wei:
65
+ return w / w.sum() # normalized weights
66
+ else:
67
+ return w.sum() # effective number of sequences
68
+
69
+
70
+ def helmert_basis(k: int) -> np.ndarray:
71
+ # k x (k-1), columns span {v : 1^T v = 0}, columns orthonormal
72
+ H = np.zeros((k, k-1), float)
73
+ for j in range(k-1):
74
+ H[:j+1, j] = 1.0
75
+ H[j+1, j] = -(j+1)
76
+ H[:j+2, j] /= np.sqrt((j+1)*(j+2))
77
+ return H
78
+
79
+
80
+ def to_zero_sum(X: np.ndarray, L: int, k: int) -> np.ndarray:
81
+ # X: N x (kL) one-hot, L positions, k symbols
82
+ Q = helmert_basis(k) # k x (k-1)
83
+ T = np.kron(np.eye(L), Q) # (kL) x (L*(k-1))
84
+ return X @ T # N x L*(k-1)
85
+
86
+
87
+ def weighted_covariance(Z, w):
88
+ # Z: N×M, w: N, sum(w)=1
89
+ Zt = Z * np.sqrt(w[:, None]) # weighted data: diag(√w) Z
90
+ mu = (w @ Z) # μ_w ∈ ℝ^{M×1}
91
+ C = Zt.T @ Zt - np.outer(mu, mu) # Zᵀ diag(w) Z − μ_w μ_wᵀ
92
+ return C, mu, Zt
93
+
94
+
95
+ def get_center_data(X, w):
96
+ mu = w @ X # (M,)
97
+ Zx = X - mu # center
98
+ Zw = np.sqrt(w)[:, None] * Zx # sqrt(W) Zc
99
+ return Zw
100
+
101
+
102
+ def effective_length(
103
+ Z, w=None, tol=1e-12, signal_frac=None,
104
+ zs=True, neff=False, reg=0., svd=True
105
+ ):
106
+ """
107
+ Estimate the effective dimensional length of a dataset via the entropy of
108
+ its variance spectrum.
109
+
110
+ Parameters
111
+ ----------
112
+ Z : array_like, shape (N, L, k)
113
+ Input tensor of N samples with L positions and k features per position.
114
+ w : array_like, optional
115
+ Sample weights of length N. If None, uniform weights are used.
116
+ tol : float, optional
117
+ Absolute numerical eigenvalue threshold. Components below this value
118
+ are discarded before any signal-fraction truncation.
119
+ signal_frac : float or None, optional
120
+ Fraction of total retained variance to preserve. If in (0, 1], the
121
+ spectrum is sorted in decreasing order and truncated to the smallest
122
+ number of components whose cumulative variance is at least
123
+ `signal_frac`. For example, `signal_frac=0.99` retains components
124
+ explaining 99% of the variance.
125
+ zs : bool, optional
126
+ If True, apply zero-sum mean-removal constraint across features.
127
+ neff : bool, optional
128
+ If True, return a support estimate of the sequence space spanned by
129
+ the MSA.
130
+ reg : float, optional
131
+ Diagonal regularization added to covariance in eigenvalue mode.
132
+ svd : bool, optional
133
+ If True, or when N < 5M, use SVD of centered data; otherwise use
134
+ weighted covariance eigenvalues.
135
+
136
+ Returns
137
+ -------
138
+ float
139
+ Entropy-based effective rank of the feature space, optionally scaled
140
+ to an effective alphabet-size support when `neff=True`.
141
+
142
+ Notes
143
+ -----
144
+ The method computes the Shannon entropy of normalized singular values or
145
+ covariance eigenvalues and converts it to an effective dimension called
146
+ `L_eff`.
147
+ """
148
+ N, L, k = Z.shape
149
+ Z = Z.reshape(N, -1)
150
+
151
+ if zs:
152
+ Z = to_zero_sum(Z, L, k)
153
+
154
+ N, M = Z.shape
155
+
156
+ if w is None:
157
+ w = np.ones(N) / N
158
+
159
+ if svd or N < 5 * M:
160
+ Zw = get_center_data(Z, w)
161
+ vals = np.linalg.svd(Zw, full_matrices=False, compute_uv=False)
162
+ vals = (vals ** 2) / Zw.shape[0]
163
+ else:
164
+ C, mu, Zt = weighted_covariance(Z, w)
165
+ vals = np.linalg.eigvalsh((C + C.T) / 2.0 + reg * np.eye(M))
166
+
167
+ vals = vals[vals > tol]
168
+
169
+ if vals.size == 0:
170
+ return 0.0
171
+
172
+ vals = np.sort(vals)[::-1]
173
+
174
+ if signal_frac is not None:
175
+ if not (0.0 < signal_frac <= 1.0):
176
+ raise ValueError("signal_frac must be in (0, 1].")
177
+
178
+ frac = np.cumsum(vals) / np.sum(vals)
179
+ n_keep = np.searchsorted(frac, signal_frac) + 1
180
+ vals = vals[:n_keep]
181
+
182
+ p = vals / vals.sum()
183
+ H = -np.sum(p * np.log(p))
184
+
185
+ denom = k - 1 if zs else k
186
+ Leff = np.exp(H) / denom
187
+
188
+ if neff:
189
+ return k ** Leff
190
+ return float(Leff)
191
+
192
+
193
+ def spectral_entropy(C, tol=1e-12):
194
+ lam_full, U_full = np.linalg.eigh(C) # M eigenvalues, M×M eigenvectors
195
+ mask = lam_full > tol
196
+ lam = lam_full[mask] # r
197
+ U = U_full[:, mask] # M×r
198
+
199
+ S = lam.sum()
200
+ p = lam / S
201
+ H = -np.sum(p * np.log(p))
202
+ return H, lam, U
203
+
204
+
205
+ def cross_effective_length(X, Y, wx=None, wy=None, tol=1e-12, zs=True, alpha=1.):
206
+ """
207
+ Cross isotropy between two *independent* sample sets.
208
+ X: (Nx, Lx, k)
209
+ Y: (Ny, Ly, k)
210
+ """
211
+
212
+ Nx, Lx, k = X.shape
213
+ Ny, Ly, k = Y.shape
214
+
215
+ X = to_zero_sum(X.reshape(Nx, -1), Lx, k) if zs else X
216
+ Y = to_zero_sum(Y.reshape(Ny, -1), Ly, k) if zs else Y
217
+
218
+ if wx is None: wx = np.ones(Nx) / Nx
219
+ if wy is None: wy = np.ones(Ny) / Ny
220
+ wx = wx / wx.sum()
221
+ wy = wy / wy.sum()
222
+
223
+ Xc, Yc = get_center_data(X, wx), get_center_data(Y, wy)
224
+ # independent cross moment
225
+ Cxy = Xc @ Yc.T
226
+
227
+ # singular spectrum
228
+ s = np.linalg.svd(Cxy, compute_uv=False)
229
+ s = s[s > tol]
230
+ if s.size == 0:
231
+ return 0.0
232
+
233
+ p = s / s.sum()
234
+
235
+ if alpha == 1:
236
+ H = -np.sum(p * np.log(p))
237
+ eff_rank = np.exp(H)
238
+ else:
239
+ eff_rank = np.sum(p**alpha) ** (1.0 / (1.0 - alpha))
240
+
241
+ return float(eff_rank / (k - 1))
242
+
243
+
244
+ def average_min_dist(Y, X):
245
+ if len(X.shape) == 3:
246
+ Xf, Yf = X.reshape(len(X), -1), Y.reshape(len(Y), -1)
247
+ else:
248
+ Xf, Yf = X, Y
249
+
250
+ XY = Xf @ Yf.T
251
+ XX = Xf @ Xf.T
252
+ np.fill_diagonal(XX, 0)
253
+ return (XY.max(1)[XX.max(1)>0] / XX.max(1)[XX.max(1)>0]).mean()
254
+
255
+
256
+ def leff(msa: list, seq_type: str, **kwargs) -> float:
257
+ return effective_length(msa_to_oh(msa, seq_type=seq_type), **kwargs)
258
+
259
+
260
+ def cross_leff(msa_x: list, msa_y: list, seq_type: str, **kwargs) -> float:
261
+ return cross_effective_length(msa_to_oh(msa_x, seq_type=seq_type), msa_to_oh(msa_y, seq_type=seq_type), **kwargs)
262
+
eff_len/utils.py ADDED
@@ -0,0 +1,93 @@
1
+ """
2
+ """
3
+
4
+ import numpy as np
5
+ import re
6
+
7
+ # alphabets
8
+ NUC = {'-': 0, 'A': 1, 'U': 2, 'C': 3, 'G': 4}
9
+ INDEX_TO_NUC = {v: k for k, v in NUC.items()}
10
+
11
+ AA = {'-': 0, 'A': 1, 'R': 2, 'N': 3, 'D': 4, 'C': 5, 'Q': 6, 'E': 7, 'G': 8,
12
+ 'H': 9, 'I': 10, 'L': 11, 'K': 12, 'M': 13, 'F': 14, 'P': 15, 'S': 16,
13
+ 'T': 17, 'W': 18, 'Y': 19, 'V': 20}
14
+ INDEX_TO_AA = {v: k for k, v in AA.items()}
15
+
16
+
17
+ def hamming_distance(s1, s2):
18
+ return sum(a != b for a, b in zip(s1, s2))
19
+
20
+
21
+ def seq_to_indices(sequence, seq_type="nuc"):
22
+ if seq_type == "nuc":
23
+ alphabet = NUC
24
+ elif seq_type == "prot":
25
+ alphabet = AA
26
+ else:
27
+ raise ValueError("seq_type must be 'nuc' or 'prot'")
28
+ return np.array([alphabet[x] for x in sequence], dtype=int)
29
+
30
+
31
+ def get_one_hot(data, num_classes):
32
+ N, L = data.shape
33
+ oh = np.zeros((N, L, num_classes), dtype=float)
34
+ oh[np.arange(N)[:, None], np.arange(L), data] = 1.0
35
+ return oh
36
+
37
+
38
+ def read_fasta(infile, seq_type="prot"):
39
+ results = {}
40
+ with open(infile, 'r') as f:
41
+ name = None
42
+ for l in f:
43
+ l = l.strip()
44
+ if l.startswith(">"):
45
+ name = l[1:]
46
+ results[name] = ""
47
+ elif name:
48
+ if seq_type == "nuc":
49
+ cleaned = re.sub(r'[^ACGUT]', '-', l.upper()).replace("T", "U")
50
+ elif seq_type == "prot":
51
+ cleaned = re.sub(r'[^ACDEFGHIKLMNPQRSTVWY]', '-', l.upper())
52
+ else:
53
+ cleaned = l.upper()
54
+ results[name] += cleaned
55
+ return results
56
+
57
+
58
+ def msa_to_oh(msa, seq_type="nuc"):
59
+ if seq_type == "nuc":
60
+ k = len(NUC) # 5
61
+ elif seq_type == "prot":
62
+ k = len(AA) # 21
63
+ else:
64
+ raise ValueError("seq_type must be 'nuc' or 'prot'")
65
+
66
+ if isinstance(msa, dict):
67
+ msa_l = [seq_to_indices(seq, seq_type) for seq in msa.values()]
68
+ else:
69
+ msa_l = [seq_to_indices(seq, seq_type) for seq in msa]
70
+ msa_arr = np.stack(msa_l) # (N, L)
71
+ return get_one_hot(msa_arr, num_classes=k) # (N, L, k)
72
+
73
+
74
+ def oh_to_msa(msa_oh, seq_type="nuc"):
75
+ if seq_type == "nuc":
76
+ idx_to_sym = INDEX_TO_NUC
77
+ elif seq_type == "prot":
78
+ idx_to_sym = INDEX_TO_AA
79
+ else:
80
+ raise ValueError("seq_type must be 'nuc' or 'prot'")
81
+
82
+ idxs = np.argmax(msa_oh, axis=-1) # (N, L)
83
+ return [''.join(idx_to_sym[i] for i in row) for row in idxs]
84
+
85
+
86
+ def msa_to_ind(msa, seq_type="nuc"):
87
+ if type(msa) is dict:
88
+ msa = msa.values()
89
+ return np.stack([seq_to_indices(seq, seq_type) for seq in msa])
90
+
91
+
92
+ def ind_to_msa(ind_array):
93
+ return ["".join(INDEX_TO_NUC[i] for i in row) for row in ind_array]
@@ -0,0 +1,91 @@
1
+ Metadata-Version: 2.5
2
+ Name: eff-len
3
+ Version: 0.1.0
4
+ Summary: Spectral measure of diversity for multiple sequence alignments
5
+ Project-URL: Homepage, https://github.com/vaiteaopuu/effective_length
6
+ Project-URL: Issues, https://github.com/vaiteaopuu/effective_length/issues
7
+ Author-email: Vaitea Opuu <vaitea.opuu@cnrs.fr>
8
+ License-Expression: MIT
9
+ Keywords: bioinformatics,diversity,msa,sequence-alignment
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
13
+ Requires-Python: >=3.9
14
+ Requires-Dist: numpy
15
+ Description-Content-Type: text/markdown
16
+
17
+ # eff_len
18
+
19
+ Spectral measure of diversity for multiple sequence alignments.
20
+
21
+ ## Overview
22
+
23
+ This repo contains the code to reproduce the results in *"A spectral framework for measuring diversity in multiple sequence alignments"*. It introduces a measure, `L_eff`, that estimates the diversity (or amount of information) contained in a multiple sequence alignment. `L_eff` allows a faithful comparison between MSAs, as well as between generated datasets.
24
+
25
+ ## Install
26
+
27
+ Requirements: NumPy.
28
+
29
+ ```bash
30
+ pip install eff_len
31
+ ```
32
+
33
+ From source:
34
+
35
+ ```bash
36
+ git clone https://github.com/vaiteaopuu/effective_length
37
+ cd effective_length
38
+ pip install .
39
+ ```
40
+
41
+ ## Usage
42
+
43
+ ### Python
44
+
45
+ ```python
46
+ from eff_len import read_fasta, msa_to_oh, effective_length
47
+
48
+ msa = read_fasta("data/RF00028.fa", seq_type="nuc")
49
+
50
+ msa_oh = msa_to_oh(msa, seq_type="nuc")
51
+ N, L, k = msa_oh.shape
52
+ L_eff = effective_length(msa_oh)
53
+
54
+ print(N, L, L_eff, L_eff / L)
55
+ # 2611 251 35.88477058938092 0.14296721350350963
56
+ ```
57
+
58
+ `cross_effective_length` and `leff` are also exported, for comparing two alignments and for the convenience wrapper respectively.
59
+
60
+ ## Repository content
61
+
62
+ | Path | Description |
63
+ | --- | --- |
64
+ | `src/eff_len/` | The package itself |
65
+ | `analysis/` | Notebooks and scripts |
66
+ | `data/` | Example alignments |
67
+ | `reproducibility.org` | Code snippets reproducing the figures in the paper |
68
+
69
+ ## Data sources
70
+
71
+ The data used in these analyses were extracted from:
72
+
73
+ - C. Lambert *et al.* (2025) *Nat. Commun.*
74
+ - F. Calvanese *et al.* (2024) *NAR*
75
+ - M. Mirdita *et al.* (2027) *NAR*
76
+
77
+ ## Citation
78
+
79
+ If you use this code, please cite:
80
+
81
+ ```bibtex
82
+ @article{opuu_spectral,
83
+ title = {A spectral framework for measuring diversity in multiple sequence alignments},
84
+ author = {Opuu, Vaitea},
85
+ year = {2026}
86
+ }
87
+ ```
88
+
89
+ ## License
90
+
91
+ MIT
@@ -0,0 +1,6 @@
1
+ eff_len/__init__.py,sha256=qqpx4c0Vgdu_7ScKZHgWVmk-kFXI6OscP6u_vwKQZl0,40
2
+ eff_len/div.py,sha256=-dpj75TqvG360XODJKPiV6W7U2MRzIIFo87ztGgyZmY,7642
3
+ eff_len/utils.py,sha256=FN81ScLplmNptDItm7et36jaEKRyKrWUSr1SBC5d6f4,2703
4
+ eff_len-0.1.0.dist-info/METADATA,sha256=_LjcQH53z9I4HHZIdXZ31Dgmp4GtDNcYg69vFw7nQpA,2425
5
+ eff_len-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
6
+ eff_len-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any