pdukit 1.0.0a1__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.
- pdukit/__init__.py +5 -0
- pdukit/__main__.py +7 -0
- pdukit/cli.py +96 -0
- pdukit/lib/__init__.py +1 -0
- pdukit/lib/pdu/__init__.py +10 -0
- pdukit/lib/pdu/benchmark.py +362 -0
- pdukit/lib/pdu/cog2.py +1251 -0
- pdukit/lib/pdu/cog2_checks.py +837 -0
- pdukit/lib/pdu/cog2_cli.py +709 -0
- pdukit/lib/pdu/cog2_cohesion.py +421 -0
- pdukit/lib/pdu/cog2_compare.py +75 -0
- pdukit/lib/pdu/cog2_data.py +632 -0
- pdukit/lib/pdu/cog2_faces.py +269 -0
- pdukit/lib/pdu/cog2_fasttest.py +406 -0
- pdukit/lib/pdu/cog2_gate.py +130 -0
- pdukit/lib/pdu/cog2_gpu.py +279 -0
- pdukit/lib/pdu/cog2_graph.py +154 -0
- pdukit/lib/pdu/cog2_ledger.py +162 -0
- pdukit/lib/pdu/cog2_outputs.py +193 -0
- pdukit/lib/pdu/cog2_pdudef.py +386 -0
- pdukit/lib/pdu/cog2_pipeline.py +1404 -0
- pdukit/lib/pdu/cog2_resolve.py +247 -0
- pdukit/lib/pdu/cog2_run.py +49 -0
- pdukit/lib/pdu/cog2_scoring.py +290 -0
- pdukit/lib/pdu/cog2_stability.py +561 -0
- pdukit/lib/pdu/cog2_summarise.py +99 -0
- pdukit/lib/pdu/extract/__init__.py +1 -0
- pdukit/lib/pdu/extract/hdf5_layout.py +415 -0
- pdukit/lib/pdu/extract/io.py +275 -0
- pdukit/lib/pdu/extract/remote_io.py +141 -0
- pdukit/lib/pdu/flux_pilot.py +1604 -0
- pdukit/lib/pdu/interface.py +3165 -0
- pdukit/lib/pdu/null_calibration_cpu.py +611 -0
- pdukit/lib/pdu/null_calibration_gpu.py +424 -0
- pdukit/lib/pdu/null_calibration_run.py +789 -0
- pdukit/lib/pdu/ppf.py +172 -0
- pdukit/lib/pdu/schur.py +491 -0
- pdukit/lib/pdu/tests/__init__.py +0 -0
- pdukit/lib/pdu/tests/test_cli.py +77 -0
- pdukit/lib/pdu/tests/test_cog2.py +755 -0
- pdukit/lib/pdu/tests/test_cog2_fixes.py +267 -0
- pdukit/lib/pdu/tests/test_interface.py +898 -0
- pdukit/params/syt_c2a_channels.ppf +122 -0
- pdukit/params/syt_c2a_cohesion.ppf +122 -0
- pdukit/params/t4l_channels.ppf +122 -0
- pdukit/params/t4l_cohesion.ppf +122 -0
- pdukit-1.0.0a1.dist-info/METADATA +140 -0
- pdukit-1.0.0a1.dist-info/RECORD +52 -0
- pdukit-1.0.0a1.dist-info/WHEEL +5 -0
- pdukit-1.0.0a1.dist-info/entry_points.txt +5 -0
- pdukit-1.0.0a1.dist-info/licenses/LICENSE +675 -0
- pdukit-1.0.0a1.dist-info/top_level.txt +1 -0
pdukit/__init__.py
ADDED
pdukit/__main__.py
ADDED
pdukit/cli.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""The ``pdu`` command: one entry point with a module per task.
|
|
2
|
+
|
|
3
|
+
pdu run -f R1.xtc R2.xtc -s top.tpr -p t4l.ppf -o out
|
|
4
|
+
pdu bench -p bench.ppf -o out (planted system, known answer)
|
|
5
|
+
pdu check -f ... -s ... -p t4l.ppf (resolve and print; run nothing)
|
|
6
|
+
pdu summarise RESULTS_ROOT
|
|
7
|
+
pdu compare RUN_DIR RUN_DIR ...
|
|
8
|
+
pdu fasttest --out DIR ...
|
|
9
|
+
|
|
10
|
+
Settings live in a parameter file (``-p``, ``key = value``, see
|
|
11
|
+
`pdukit.lib.pdu.ppf`); a command-line option overrides the file.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
MODULES = {
|
|
19
|
+
"run": "run the pipeline on MD trajectories",
|
|
20
|
+
"bench": "run the pipeline on a planted system with a known answer",
|
|
21
|
+
"check": "resolve all settings and print them; run nothing",
|
|
22
|
+
"summarise": "one table from many runs",
|
|
23
|
+
"compare": "do several runs find the same units?",
|
|
24
|
+
"fasttest": "the fast test grid of planted designs",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _version():
|
|
29
|
+
try:
|
|
30
|
+
from importlib.metadata import version
|
|
31
|
+
|
|
32
|
+
return version("pdukit")
|
|
33
|
+
except Exception: # not installed (running from a checkout)
|
|
34
|
+
return "unknown"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _usage():
|
|
38
|
+
lines = [f"usage: pdu <module> [options] (pdukit {_version()})", ""]
|
|
39
|
+
lines += [f" {name:<10} {text}" for name, text in MODULES.items()]
|
|
40
|
+
lines += ["", "pdu <module> -h shows the options of a module."]
|
|
41
|
+
return "\n".join(lines)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _run(argv, planted=False):
|
|
45
|
+
from .lib.pdu.cog2_cli import parse_args
|
|
46
|
+
from .lib.pdu.cog2_pipeline import Cog2Run
|
|
47
|
+
|
|
48
|
+
args = parse_args(
|
|
49
|
+
argv + (["--planted"] if planted else []),
|
|
50
|
+
description="pdu run: detect protein dynamical units",
|
|
51
|
+
)
|
|
52
|
+
return Cog2Run(args).run()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _check(argv):
|
|
56
|
+
from .lib.pdu.cog2_cli import parse_args
|
|
57
|
+
|
|
58
|
+
args = parse_args(
|
|
59
|
+
argv,
|
|
60
|
+
description="pdu check: resolve all settings and print them",
|
|
61
|
+
require_outdir=False,
|
|
62
|
+
)
|
|
63
|
+
sys.stdout.write(args._ppf)
|
|
64
|
+
return 0
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def main(argv=None):
|
|
68
|
+
"""Dispatch ``pdu <module> ...``; return the exit code."""
|
|
69
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
70
|
+
if not argv or argv[0] in ("-h", "--help", "help"):
|
|
71
|
+
print(_usage())
|
|
72
|
+
return 0
|
|
73
|
+
if argv[0] in ("-V", "--version"):
|
|
74
|
+
print(f"pdukit {_version()}")
|
|
75
|
+
return 0
|
|
76
|
+
module, rest = argv[0], argv[1:]
|
|
77
|
+
if module == "run":
|
|
78
|
+
return _run(rest)
|
|
79
|
+
if module == "bench":
|
|
80
|
+
return _run(rest, planted=True)
|
|
81
|
+
if module == "check":
|
|
82
|
+
return _check(rest)
|
|
83
|
+
if module == "summarise":
|
|
84
|
+
from .lib.pdu.cog2_summarise import main as run_module
|
|
85
|
+
elif module == "compare":
|
|
86
|
+
from .lib.pdu.cog2_compare import main as run_module
|
|
87
|
+
elif module == "fasttest":
|
|
88
|
+
from .lib.pdu.cog2_fasttest import main as run_module
|
|
89
|
+
else:
|
|
90
|
+
print(f"pdu: unknown module {module!r}\n\n{_usage()}", file=sys.stderr)
|
|
91
|
+
return 2
|
|
92
|
+
return run_module(rest) or 0
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
if __name__ == "__main__":
|
|
96
|
+
sys.exit(main())
|
pdukit/lib/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Library code of pdukit."""
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""PDU (Protein Dynamical Unit, the *dynamical* concept).
|
|
2
|
+
|
|
3
|
+
See the top-level README.
|
|
4
|
+
|
|
5
|
+
extract/ per-domain/trajectory -> PDU candidates (the D9 pipeline)
|
|
6
|
+
analyze/ cross-domain -> does the PDU vocabulary stay finite? (K3)
|
|
7
|
+
extractor.py PDUExtractor -- class facade over extract/
|
|
8
|
+
analyzer.py PDUAnalyzer -- class facade over analyze/
|
|
9
|
+
cli.py argv dispatcher used by ../../bin/pdu
|
|
10
|
+
"""
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"""COG 2.1 Stage 1: planted benchmark systems with a known answer.
|
|
2
|
+
|
|
3
|
+
A decomposition method can only be trusted on proteins after it has been
|
|
4
|
+
shown to recover units that were put there on purpose. This module builds
|
|
5
|
+
synthetic "proteins" whose dynamical units, couplings and boundary residues
|
|
6
|
+
are known exactly, in the same (frames, residues, features) layout the real
|
|
7
|
+
pipeline uses, so every COG version can be run on them unchanged.
|
|
8
|
+
|
|
9
|
+
CONSTRUCTION (a linear stochastic watch: wheels + gears).
|
|
10
|
+
|
|
11
|
+
* Each planted unit alpha has a private latent state z_alpha of dimension
|
|
12
|
+
`latent_dim`, with dense internal dynamics A_alpha (every residue of the
|
|
13
|
+
unit feels every internal mode -- the unit is cohesive by construction).
|
|
14
|
+
* Units are joined by COUPLINGS (src -> dst, rank r): a rank-r block
|
|
15
|
+
K[dst, src] = g * U V'. Direction matters: a coupling 0 -> 1 lets unit 0
|
|
16
|
+
drive unit 1, not the reverse. This is the gear between two wheels.
|
|
17
|
+
* The whole latent state evolves as a stable VAR(1),
|
|
18
|
+
z_{t+1} = K z_t + noise.
|
|
19
|
+
* Residue i of unit alpha READS OUT its unit's latent state through its own
|
|
20
|
+
random mixing matrix plus private observation noise:
|
|
21
|
+
x_i(t) = W_i z_alpha(t) + e_i(t) (m features per residue)
|
|
22
|
+
* MULTI-COLOUR FACE residues (`face_residues` > 0) sit on the boundary of
|
|
23
|
+
two coupled units and read out BOTH: half their features from one unit,
|
|
24
|
+
half from the other. They belong to both units in the truth.
|
|
25
|
+
* Optional `decoy_pairs` place two units side by side in space with NO
|
|
26
|
+
coupling between them -- a compact blob that is not a unit. A method
|
|
27
|
+
that merges them is rewarding spatial compactness, not dynamics.
|
|
28
|
+
|
|
29
|
+
GEOMETRY. Units are placed as small balls along a chain in 3D; residues get
|
|
30
|
+
coordinates inside their unit's ball, and the contact graph is built from
|
|
31
|
+
distances, so neighbouring units touch at their faces exactly as domains do.
|
|
32
|
+
With `discontinuous=True` each unit's residues are split into two sequence
|
|
33
|
+
segments far apart in the chain, as in lysozyme's discontinuous domains --
|
|
34
|
+
spatially compact, sequence-discontinuous.
|
|
35
|
+
|
|
36
|
+
THE ANSWER (`truth`). Units as residue lists (face residues listed in both),
|
|
37
|
+
couplings with direction and rank, face residues, decoy pairs, and K itself.
|
|
38
|
+
|
|
39
|
+
Nothing here is used by the estimators; it is a test bed.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
import numpy as np
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _stable_internal(d, rho_range, rng):
|
|
48
|
+
"""Dense internal dynamics with spectral radius drawn from rho_range.
|
|
49
|
+
|
|
50
|
+
A random (non-normal) matrix rescaled, as in test_interface.simulate, so
|
|
51
|
+
internal modes mix every latent coordinate (cohesive unit).
|
|
52
|
+
"""
|
|
53
|
+
M = rng.normal(size=(d, d))
|
|
54
|
+
rho = float(rng.uniform(*rho_range))
|
|
55
|
+
return M / np.abs(np.linalg.eigvals(M)).max() * rho
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _unit_vectors(d, r, rng):
|
|
59
|
+
Q, _ = np.linalg.qr(rng.normal(size=(d, r)))
|
|
60
|
+
return Q[:, :r]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _connect_components(adj, coords):
|
|
64
|
+
"""Join disconnected components of the contact graph at their closest
|
|
65
|
+
residue pair, so ball growth can reach every residue.
|
|
66
|
+
"""
|
|
67
|
+
N = adj.shape[0]
|
|
68
|
+
while True:
|
|
69
|
+
seen = np.zeros(N, dtype=bool)
|
|
70
|
+
comp = []
|
|
71
|
+
for s in range(N):
|
|
72
|
+
if seen[s]:
|
|
73
|
+
continue
|
|
74
|
+
stack, members = [s], []
|
|
75
|
+
seen[s] = True
|
|
76
|
+
while stack:
|
|
77
|
+
u = stack.pop()
|
|
78
|
+
members.append(u)
|
|
79
|
+
for v in np.where(adj[u])[0]:
|
|
80
|
+
if not seen[v]:
|
|
81
|
+
seen[v] = True
|
|
82
|
+
stack.append(int(v))
|
|
83
|
+
comp.append(members)
|
|
84
|
+
if len(comp) <= 1:
|
|
85
|
+
return adj
|
|
86
|
+
a, b = comp[0], [i for c in comp[1:] for i in c]
|
|
87
|
+
d = np.linalg.norm(
|
|
88
|
+
coords[a][:, None, :] - coords[b][None, :, :], axis=-1
|
|
89
|
+
)
|
|
90
|
+
i, j = np.unravel_index(int(np.argmin(d)), d.shape)
|
|
91
|
+
adj[a[i], b[j]] = adj[b[j], a[i]] = True
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def default_couplings(n_units, rank=1):
|
|
95
|
+
"""Return a ring of directed rank-`rank` gears.
|
|
96
|
+
|
|
97
|
+
The direction alternates, plus one two-way link, so the benchmark has
|
|
98
|
+
units that only drive, units that only listen, and units that do both.
|
|
99
|
+
"""
|
|
100
|
+
cpl = []
|
|
101
|
+
for a in range(n_units - 1):
|
|
102
|
+
if a % 2 == 0:
|
|
103
|
+
cpl.append((a, a + 1, rank))
|
|
104
|
+
else:
|
|
105
|
+
cpl.append((a + 1, a, rank))
|
|
106
|
+
if n_units >= 2:
|
|
107
|
+
cpl.append((1, 0, rank)) # 0 <-> 1 is the two-way link
|
|
108
|
+
return cpl
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def planted_system(
|
|
112
|
+
n_units=4,
|
|
113
|
+
unit_size=8,
|
|
114
|
+
m=4,
|
|
115
|
+
latent_dim=5,
|
|
116
|
+
n_frames=6000,
|
|
117
|
+
n_replicas=3,
|
|
118
|
+
couplings=None,
|
|
119
|
+
coupling_gain=0.35,
|
|
120
|
+
internal_rho=(0.80, 0.92),
|
|
121
|
+
noise=0.3,
|
|
122
|
+
obs_noise=0.25,
|
|
123
|
+
face_residues=0,
|
|
124
|
+
decoy_pairs=(),
|
|
125
|
+
discontinuous=False,
|
|
126
|
+
unit_radius=5.0,
|
|
127
|
+
unit_spacing=9.0,
|
|
128
|
+
contact_cutoff=8.0,
|
|
129
|
+
burn_in=500,
|
|
130
|
+
seed=0,
|
|
131
|
+
dtype=np.float32,
|
|
132
|
+
):
|
|
133
|
+
"""Build a planted system.
|
|
134
|
+
|
|
135
|
+
Returns a dict:
|
|
136
|
+
|
|
137
|
+
feats (n_replicas * n_frames, N, m) residue features
|
|
138
|
+
segments [(start, end)] one per replica
|
|
139
|
+
coords (N, 3) residue positions (one static structure)
|
|
140
|
+
adjacency (N, N) bool contact graph
|
|
141
|
+
truth {"units": [...], "couplings": [(src, dst, rank)],
|
|
142
|
+
"face_residues": [...], "decoy_pairs": [...],
|
|
143
|
+
"K": latent transition matrix, "latent_dim": int,
|
|
144
|
+
"unit_of_latent": [...]}
|
|
145
|
+
config every argument, for the record
|
|
146
|
+
|
|
147
|
+
`couplings` is a list of (src, dst, rank); default `default_couplings`.
|
|
148
|
+
`decoy_pairs` is a list of (a, b) unit pairs that must be UNCOUPLED: any
|
|
149
|
+
coupling between them is removed, and they are placed adjacent in space.
|
|
150
|
+
"""
|
|
151
|
+
rng = np.random.default_rng(seed)
|
|
152
|
+
n_units, unit_size, m = int(n_units), int(unit_size), int(m)
|
|
153
|
+
d = int(latent_dim)
|
|
154
|
+
if couplings is None:
|
|
155
|
+
couplings = default_couplings(n_units)
|
|
156
|
+
decoy = [tuple(sorted(map(int, p))) for p in decoy_pairs]
|
|
157
|
+
couplings = [
|
|
158
|
+
(int(s), int(t), int(r))
|
|
159
|
+
for s, t, r in couplings
|
|
160
|
+
if tuple(sorted((int(s), int(t)))) not in decoy
|
|
161
|
+
]
|
|
162
|
+
for s, t, r in couplings:
|
|
163
|
+
if not (0 <= s < n_units and 0 <= t < n_units) or s == t:
|
|
164
|
+
raise ValueError(f"bad coupling {(s, t, r)}")
|
|
165
|
+
if not 1 <= r <= d:
|
|
166
|
+
raise ValueError(f"coupling rank {r} must be in 1..latent_dim")
|
|
167
|
+
|
|
168
|
+
# --- latent dynamics -----------------------------------------------------
|
|
169
|
+
DL = n_units * d
|
|
170
|
+
K = np.zeros((DL, DL))
|
|
171
|
+
for a in range(n_units):
|
|
172
|
+
K[a * d : (a + 1) * d, a * d : (a + 1) * d] = _stable_internal(
|
|
173
|
+
d, internal_rho, rng
|
|
174
|
+
)
|
|
175
|
+
for s, t, r in couplings:
|
|
176
|
+
U, V = _unit_vectors(d, r, rng), _unit_vectors(d, r, rng)
|
|
177
|
+
K[t * d : (t + 1) * d, s * d : (s + 1) * d] += coupling_gain * U @ V.T
|
|
178
|
+
spec = float(np.abs(np.linalg.eigvals(K)).max())
|
|
179
|
+
if spec >= 0.98:
|
|
180
|
+
K *= 0.97 / spec
|
|
181
|
+
|
|
182
|
+
# --- residue layout
|
|
183
|
+
# --------------------------------------------------------
|
|
184
|
+
N = n_units * unit_size
|
|
185
|
+
order = np.arange(N) # noqa: F841
|
|
186
|
+
if discontinuous:
|
|
187
|
+
# unit a gets two half-segments: one in the first half of the chain,
|
|
188
|
+
# one in the second, in the same unit order -- far apart in sequence.
|
|
189
|
+
h1 = unit_size // 2
|
|
190
|
+
h2 = unit_size - h1
|
|
191
|
+
first = [list(range(a * h1, (a + 1) * h1)) for a in range(n_units)]
|
|
192
|
+
off = n_units * h1
|
|
193
|
+
second = [
|
|
194
|
+
list(range(off + a * h2, off + (a + 1) * h2))
|
|
195
|
+
for a in range(n_units)
|
|
196
|
+
]
|
|
197
|
+
units = [sorted(first[a] + second[a]) for a in range(n_units)]
|
|
198
|
+
else:
|
|
199
|
+
units = [
|
|
200
|
+
list(range(a * unit_size, (a + 1) * unit_size))
|
|
201
|
+
for a in range(n_units)
|
|
202
|
+
]
|
|
203
|
+
owner = np.empty(N, dtype=int)
|
|
204
|
+
for a, u in enumerate(units):
|
|
205
|
+
owner[u] = a
|
|
206
|
+
|
|
207
|
+
# --- geometry: unit centres on a chain, decoy pairs made adjacent
|
|
208
|
+
# ----------
|
|
209
|
+
centres = np.zeros((n_units, 3))
|
|
210
|
+
for a in range(1, n_units):
|
|
211
|
+
step = rng.normal(size=3)
|
|
212
|
+
step[0] = abs(step[0]) + 1.0
|
|
213
|
+
centres[a] = centres[a - 1] + unit_spacing * step / np.linalg.norm(
|
|
214
|
+
step
|
|
215
|
+
)
|
|
216
|
+
coords = np.zeros((N, 3))
|
|
217
|
+
for i in range(N):
|
|
218
|
+
v = rng.normal(size=3)
|
|
219
|
+
v *= unit_radius * rng.uniform(0.2, 1.0) ** (1 / 3) / np.linalg.norm(v)
|
|
220
|
+
coords[i] = centres[owner[i]] + v
|
|
221
|
+
dist = np.linalg.norm(coords[:, None] - coords[None], axis=-1)
|
|
222
|
+
adj = (dist < contact_cutoff) & ~np.eye(N, dtype=bool)
|
|
223
|
+
for a, b in decoy:
|
|
224
|
+
# guarantee the decoy pair touches, so a compact ball can swallow both
|
|
225
|
+
ia, ib = units[a], units[b]
|
|
226
|
+
dd = dist[np.ix_(ia, ib)]
|
|
227
|
+
for k in np.argsort(dd, axis=None)[:3]:
|
|
228
|
+
i, j = np.unravel_index(int(k), dd.shape)
|
|
229
|
+
adj[ia[i], ib[j]] = adj[ib[j], ia[i]] = True
|
|
230
|
+
adj = _connect_components(adj, coords)
|
|
231
|
+
|
|
232
|
+
# --- multi-colour face residues
|
|
233
|
+
# ---------------------------------------------
|
|
234
|
+
faces = {}
|
|
235
|
+
if face_residues:
|
|
236
|
+
for s, t, _ in couplings:
|
|
237
|
+
# residues of unit t nearest unit s's centre
|
|
238
|
+
cand = [i for i in units[t] if i not in faces]
|
|
239
|
+
dc = np.linalg.norm(coords[cand] - centres[s], axis=1)
|
|
240
|
+
for i in [cand[k] for k in np.argsort(dc)[: int(face_residues)]]:
|
|
241
|
+
faces[int(i)] = (int(t), int(s)) # (home unit, second unit)
|
|
242
|
+
|
|
243
|
+
# --- simulate
|
|
244
|
+
# ---------------------------------------------------------------
|
|
245
|
+
W = rng.normal(size=(N, m, d)) / np.sqrt(d)
|
|
246
|
+
W2 = rng.normal(size=(N, m, d)) / np.sqrt(d)
|
|
247
|
+
feats = np.empty((n_replicas * n_frames, N, m), dtype=dtype)
|
|
248
|
+
segments = []
|
|
249
|
+
for rep in range(int(n_replicas)):
|
|
250
|
+
z = np.zeros(DL)
|
|
251
|
+
traj = np.empty((n_frames, DL))
|
|
252
|
+
for t in range(-int(burn_in), n_frames):
|
|
253
|
+
z = K @ z + rng.normal(scale=noise, size=DL)
|
|
254
|
+
if t >= 0:
|
|
255
|
+
traj[t] = z
|
|
256
|
+
out = np.empty((n_frames, N, m))
|
|
257
|
+
for i in range(N):
|
|
258
|
+
a = owner[i]
|
|
259
|
+
za = traj[:, a * d : (a + 1) * d]
|
|
260
|
+
x = za @ W[i].T
|
|
261
|
+
if i in faces:
|
|
262
|
+
b = faces[i][1]
|
|
263
|
+
zb = traj[:, b * d : (b + 1) * d]
|
|
264
|
+
half = m // 2
|
|
265
|
+
x[:, half:] = (zb @ W2[i].T)[:, half:]
|
|
266
|
+
out[:, i, :] = x + rng.normal(scale=obs_noise, size=(n_frames, m))
|
|
267
|
+
a0 = rep * n_frames
|
|
268
|
+
feats[a0 : a0 + n_frames] = out.astype(dtype)
|
|
269
|
+
segments.append((a0, a0 + n_frames))
|
|
270
|
+
|
|
271
|
+
truth_units = [
|
|
272
|
+
sorted(set(u) | {i for i, (h, b) in faces.items() if b == a})
|
|
273
|
+
for a, u in enumerate(units)
|
|
274
|
+
]
|
|
275
|
+
truth = {
|
|
276
|
+
"units": truth_units,
|
|
277
|
+
"home_units": [sorted(u) for u in units],
|
|
278
|
+
"couplings": couplings,
|
|
279
|
+
"face_residues": sorted(faces),
|
|
280
|
+
"face_owners": {int(i): list(v) for i, v in faces.items()},
|
|
281
|
+
"decoy_pairs": [list(p) for p in decoy],
|
|
282
|
+
"K": K,
|
|
283
|
+
"latent_dim": d,
|
|
284
|
+
"unit_of_latent": [a for a in range(n_units) for _ in range(d)],
|
|
285
|
+
}
|
|
286
|
+
config = dict(
|
|
287
|
+
n_units=n_units,
|
|
288
|
+
unit_size=unit_size,
|
|
289
|
+
m=m,
|
|
290
|
+
latent_dim=d,
|
|
291
|
+
n_frames=n_frames,
|
|
292
|
+
n_replicas=n_replicas,
|
|
293
|
+
couplings=couplings,
|
|
294
|
+
coupling_gain=coupling_gain,
|
|
295
|
+
internal_rho=tuple(internal_rho),
|
|
296
|
+
noise=noise,
|
|
297
|
+
obs_noise=obs_noise,
|
|
298
|
+
face_residues=face_residues,
|
|
299
|
+
decoy_pairs=[list(p) for p in decoy],
|
|
300
|
+
discontinuous=discontinuous,
|
|
301
|
+
seed=seed,
|
|
302
|
+
spectral_radius=float(np.abs(np.linalg.eigvals(K)).max()),
|
|
303
|
+
)
|
|
304
|
+
return {
|
|
305
|
+
"feats": feats,
|
|
306
|
+
"segments": segments,
|
|
307
|
+
"coords": coords,
|
|
308
|
+
"adjacency": adj,
|
|
309
|
+
"truth": truth,
|
|
310
|
+
"config": config,
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def match_units(found, truth_units, *, threshold=0.67):
|
|
315
|
+
"""Compare a decomposition with the planted answer.
|
|
316
|
+
|
|
317
|
+
For every TRUE unit, the best Jaccard overlap with any found unit
|
|
318
|
+
(recall side); for every FOUND unit, the best overlap with any true unit
|
|
319
|
+
(precision side). A true unit counts as recovered when its best overlap
|
|
320
|
+
reaches `threshold`. Also reports how many found units straddle two true
|
|
321
|
+
units (each overlapping by >= 25 % of the found unit) -- the merge error.
|
|
322
|
+
The default threshold is 0.67: at 0.5 a merged 20-residue unit counted
|
|
323
|
+
as recovering both of the 10-residue units inside it.
|
|
324
|
+
"""
|
|
325
|
+
F = [set(map(int, u)) for u in found]
|
|
326
|
+
T = [set(map(int, u)) for u in truth_units]
|
|
327
|
+
|
|
328
|
+
def jac(a, b):
|
|
329
|
+
return len(a & b) / max(len(a | b), 1)
|
|
330
|
+
|
|
331
|
+
rec = [max((jac(t, f) for f in F), default=0.0) for t in T]
|
|
332
|
+
prec = [max((jac(f, t) for t in T), default=0.0) for f in F]
|
|
333
|
+
merged = 0
|
|
334
|
+
for f in F:
|
|
335
|
+
big = [t for t in T if len(f & t) >= 0.25 * max(len(f), 1)]
|
|
336
|
+
merged += int(len(big) >= 2)
|
|
337
|
+
return {
|
|
338
|
+
"recall_jaccard": [round(x, 4) for x in rec],
|
|
339
|
+
"precision_jaccard": [round(x, 4) for x in prec],
|
|
340
|
+
"mean_recall": float(np.mean(rec)) if rec else 0.0,
|
|
341
|
+
"mean_precision": float(np.mean(prec)) if prec else 0.0,
|
|
342
|
+
"n_recovered": int(sum(x >= threshold for x in rec)),
|
|
343
|
+
"n_true": len(T),
|
|
344
|
+
"n_found": len(F),
|
|
345
|
+
"n_merged": int(merged),
|
|
346
|
+
"threshold": float(threshold),
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def face_recovery(boundary_residues, truth):
|
|
351
|
+
"""Fraction of planted multi-colour face residues that the method
|
|
352
|
+
reported as boundary (shared) residues, and how many reported boundary
|
|
353
|
+
residues are not faces.
|
|
354
|
+
"""
|
|
355
|
+
faces = set(truth.get("face_residues", []))
|
|
356
|
+
got = set(map(int, boundary_residues))
|
|
357
|
+
return {
|
|
358
|
+
"n_faces": len(faces),
|
|
359
|
+
"found": len(faces & got),
|
|
360
|
+
"fraction": (len(faces & got) / len(faces)) if faces else float("nan"),
|
|
361
|
+
"extra_boundary": len(got - faces),
|
|
362
|
+
}
|