Repository navigation
Expand file tree
/
Copy pathzip_edit.cpp
More file actions
2061 lines (1982 loc) · 90.2 KB
/
Copy pathzip_edit.cpp
File metadata and controls
2061 lines (1982 loc) · 90.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
zip_edit.cpp -- spec step 6: pieces, the left-snap projection, snapping, unaligned stretches,
acceptance (A)-(D), cuts, the splitter, the pinch, the transaction with validators V1-V7,
--check, id allocation and applying the plans.
How a site is edited
Every decision is made on the input graph. An accepted piece gives its node interval a fate:
"zipped onto [ta, tb) of target space S with rel r". The edited site is then a pure function
of (input site, fates, block-end cut points) and is built by build_model():
1. ends: every site link end, and both sides of every fate end inside a node, is an End
(node, offset, side); an End on a zipped piece is translated (the pinch):
rel '+': p.L -> L side of the target piece starting at ta; p.R -> R side of the piece ending at tb
rel '-': p.L -> R side of the target piece ending at tb; p.R -> L side of the piece starting at ta
read in the space's walk orientation and converted to node sides;
2. a translated link equal to the target's own adjacency inside a node is dropped (no cut);
3. cuts: fate ends; every remaining link end inside a node (the pieces that carry a
translated link); and P of every block end of the accepted chains;
4. the splitter: pieces inherit SN/SR, SO = parent SO + offset; links on a node's L side go
to its first piece, on its R side to its last (side-aware, both ends of a self-loop);
consecutive kept pieces of a node are joined by a new ++ link carrying the node's SR;
5. links are deduplicated under a canonical key (a link equals its reverse) with priority:
input links, then new split links, then translated links; a translated image equal to the
target's own adjacency or to an existing link is dropped;
6. zipped pieces are deleted. Nothing is wired to chain ends or snarl boundaries; an end
that resolves to no live piece is an internal error.
The validators read the model: V1 reference walk, V2 every unit's image exists link by link and
spells the predicted sequence (and, with --check, every anchored path at sites with <= 5,000),
V3 no piece twice in an image, V4 no new forward-strand SCC, V5 locality / windows / rank-0 back
edges, V6 every kept piece reaches the reference, V7 pieces tile their parents (SN/SR/SO) and,
per chain over its kept pieces, removed <= target / i + G.
Acceptance (spec 6.7) is greedy in a total order; a piece failing (A), (B) or (C) is dropped and
stays alt (it is then attached at its projected points by the translation); a chain is refused
by trimmed-below-b or (D). With observed walks (v3) a new piece also fails (G), the whole-record
rule, when a GAF record that walks its node reads its target elsewhere (WalkSource::reads_target;
the GAF audit's reading rule). The transaction (6.8) validates the whole edit and, on failure,
re-plans incrementally in acceptance order, dropping each chain whose addition fails.
A chain's report kept_bp is the bp of its new pieces (compatible pieces are counted by the chain
that zipped them first): the zipped rows sum to the bp the edit removes (checked).
P (the left-snap projection) is zip_align's project_left_snap, shared with the feasibility cut.
*/
#include "zip_edit.hpp"
#include <chrono>
#include <map>
#include <set>
#include <unordered_map>
#include <unordered_set>
namespace zip {
namespace {
inline int64_t iabs64(int64_t x) { return x < 0 ? -x : x; }
// Debug hook for test Z29 (not a CLI option): RGFA_ZIP_DEBUG_DROP_LINK="<site label>:<unit id>[,...]"
// leaves out the first translated link (canonical order) that the named unit's chain creates,
// whenever that chain is in the edit. The transaction must then revert that chain alone.
const std::vector<std::pair<std::string, uint32_t>>& debug_drop_links() {
static std::once_flag once;
static std::vector<std::pair<std::string, uint32_t>> v;
std::call_once(once, []() {
const char* e = getenv("RGFA_ZIP_DEBUG_DROP_LINK");
if (!e || !*e) return;
std::string s(e);
size_t p = 0;
while (p <= s.size()) {
size_t q = s.find(',', p);
if (q == std::string::npos) q = s.size();
std::string item = s.substr(p, q - p);
size_t c = item.rfind(':');
int64_t u = 0;
if (c == std::string::npos || !parse_int64(item.substr(c + 1), u) || u < 0)
fail(EXIT_INPUT, "RGFA_ZIP_DEBUG_DROP_LINK: expected <site label>:<unit id>[,...], got '" + item + "'");
v.push_back(std::make_pair(item.substr(0, c), (uint32_t)u));
ZLOG("debug: RGFA_ZIP_DEBUG_DROP_LINK drops one translated link of unit %u at site %s", (uint32_t)u, item.substr(0, c).c_str());
p = q + 1;
}
});
return v;
}
// ================================================================ target spaces
// An oriented walk of nodes carrying target coordinates. Element k (handle walk[k]) covers
// [c[k], c[k+1]). The first and last elements are sentinels (A and B for the reference; u and v
// for an alt branch): targets lie in [c[1], c[n-1]].
struct TSpace {
bool ref = true;
std::vector<Handle> walk;
std::vector<int64_t> c;
int64_t lo() const { return c[1]; }
int64_t hi() const { return c[c.size() - 2]; }
// element k with c[k] < T <= c[k+1]
size_t elem_ending(int64_t T) const {
return (size_t)(std::lower_bound(c.begin() + 1, c.end(), T) - c.begin()) - 1;
}
// element k with c[k] <= T < c[k+1]
size_t elem_starting(int64_t T) const {
return (size_t)(std::upper_bound(c.begin(), c.end(), T) - c.begin()) - 1;
}
};
// ================================================================ the site context
struct Ctx {
const Graph& g;
const Site& s;
const SiteData& sd;
const Options& opt;
std::vector<NodeId> nodes; // site nodes (A, B, interior), sorted
std::unordered_map<NodeId, uint32_t> index; // node -> position in nodes
std::vector<uint32_t> links; // input links with an end on a site side, sorted
std::vector<uint8_t> is_ref, orient_rev; // per site node; orient_rev: forward-strand orientation '-'
uint32_t iA = NONE, iB = NONE;
std::vector<TSpace> spaces; // [0] = the reference
Ctx(const Graph& g_, const SiteData& sd_, const Options& opt_) : g(g_), s(*sd_.site), sd(sd_), opt(opt_) {}
uint32_t idx(NodeId n) const {
auto it = index.find(n);
return it == index.end() ? NONE : it->second;
}
int64_t len(uint32_t i) const { return g.len(nodes[i]); }
};
void build_ctx(Ctx& cx) {
const Graph& g = cx.g;
const Site& s = cx.s;
cx.nodes = s.interior;
cx.nodes.push_back(s.A);
cx.nodes.push_back(s.B);
std::sort(cx.nodes.begin(), cx.nodes.end());
cx.nodes.erase(std::unique(cx.nodes.begin(), cx.nodes.end()), cx.nodes.end());
cx.index.reserve(cx.nodes.size() * 2);
for (uint32_t i = 0; i < (uint32_t)cx.nodes.size(); ++i) cx.index[cx.nodes[i]] = i;
cx.iA = cx.idx(s.A);
cx.iB = cx.idx(s.B);
// site links: every link of an interior node, plus the links on A.R and B.L
for (NodeId n : s.interior)
for (int o = 0; o < 2; ++o)
for (const Edge& e : g.out(make_handle(n, o == 1))) cx.links.push_back(e.link);
for (const Edge& e : g.out(make_handle(s.A, false))) cx.links.push_back(e.link); // A.R
for (const Edge& e : g.out(make_handle(s.B, true))) cx.links.push_back(e.link); // B.L
std::sort(cx.links.begin(), cx.links.end());
cx.links.erase(std::unique(cx.links.begin(), cx.links.end()), cx.links.end());
cx.is_ref.assign(cx.nodes.size(), 0);
cx.orient_rev.assign(cx.nodes.size(), 0);
for (uint32_t i = 0; i < (uint32_t)cx.nodes.size(); ++i) cx.is_ref[i] = g.is_ref(cx.nodes[i]) ? 1 : 0;
// forward-strand orientation of alt nodes: as the node's creator excursion walks it (canonical
// frame: anchors '+'), else as the first unit through it walks it, else '+'
{
std::vector<uint8_t> set(cx.nodes.size(), 0);
for (int pass = 0; pass < 2; ++pass)
for (const Unit& u : cx.sd.units) {
for (Handle h : u.exc.alts) {
uint32_t i = cx.idx(handle_node(h));
if (i == NONE || set[i] || cx.is_ref[i]) continue;
if (pass == 0) {
bool own = false;
const std::string& sn = g.sn_names[g.nodes[handle_node(h)].sn];
for (const Owner& o : u.exc.owners)
if (o.contig == sn) own = true;
if (!own) continue;
}
cx.orient_rev[i] = handle_rev(h) ? 1 : 0;
set[i] = 1;
}
}
}
// the reference space: A+, backbone, B+
TSpace S;
S.ref = true;
S.walk.push_back(make_handle(s.A, false));
S.c.push_back(g.start(s.A));
for (NodeId b : s.backbone) {
S.walk.push_back(make_handle(b, false));
S.c.push_back(g.start(b));
}
S.walk.push_back(make_handle(s.B, false));
S.c.push_back(g.start(s.B));
S.c.push_back(g.end(s.B));
cx.spaces.push_back(std::move(S));
}
// ================================================================ fates, ends
// A zipped node interval: [a, b) of a node lands on [ta, tb) of target space `space` with rel.
struct Fate {
int64_t a = 0, b = 0;
uint32_t space = 0;
int64_t ta = 0, tb = 0;
char rel = '+';
uint32_t chain = NONE; // candidate index
};
typedef std::map<uint32_t, std::vector<Fate>> FateMap; // site node index -> fates sorted by a
// An end of a link: the R side of the piece of node i ending at x (right), or the L side of the
// piece starting at x.
struct End {
uint32_t i = NONE;
int64_t x = 0;
bool right = false;
};
End side_end(const Ctx& cx, Side sd) {
uint32_t i = cx.idx(side_node(sd));
End e;
e.i = i;
e.right = side_is_right(sd);
e.x = e.right ? (i == NONE ? 0 : cx.len(i)) : 0;
return e;
}
// walk-R side at T: the R side (in walk orientation) of the space piece ending at T
End walk_R(const Ctx& cx, const TSpace& S, int64_t T) {
size_t k = S.elem_ending(T);
Handle h = S.walk[k];
End e;
e.i = cx.idx(handle_node(h));
int64_t d = T - S.c[k];
if (!handle_rev(h)) { e.x = d; e.right = true; }
else { e.x = cx.len(e.i) - d; e.right = false; }
return e;
}
// walk-L side at T: the L side (in walk orientation) of the space piece starting at T
End walk_L(const Ctx& cx, const TSpace& S, int64_t T) {
size_t k = S.elem_starting(T);
Handle h = S.walk[k];
End e;
e.i = cx.idx(handle_node(h));
int64_t d = T - S.c[k];
if (!handle_rev(h)) { e.x = d; e.right = false; }
else { e.x = cx.len(e.i) - d; e.right = true; }
return e;
}
// the pinch: where a side of the zipped piece [f.a, f.b) goes
End translate(const Ctx& cx, const Fate& f, bool right) {
const TSpace& S = cx.spaces[f.space];
if (!right) return f.rel == '+' ? walk_L(cx, S, f.ta) : walk_R(cx, S, f.tb);
return f.rel == '+' ? walk_R(cx, S, f.tb) : walk_L(cx, S, f.ta);
}
const Fate* fate_ending(const std::vector<Fate>& v, int64_t x) {
for (const Fate& f : v)
if (f.b == x) return &f;
return nullptr;
}
const Fate* fate_starting(const std::vector<Fate>& v, int64_t x) {
for (const Fate& f : v)
if (f.a == x) return &f;
return nullptr;
}
// ================================================================ the edited site (model)
struct MPiece {
uint32_t i = NONE; // site node index
int64_t off = 0, len = 0;
bool kept = true;
uint32_t chain = NONE; // zipped: the chain whose fate it is
};
struct MLink {
uint32_t sa = 0, sb = 0; // piece sides 2*piece + right; for class 0, sa is the input's first end
int32_t sr = -1;
uint32_t src = NONE; // input link, or NONE (split link)
uint32_t split_i = NONE; // split link: site node index and offset
int64_t split_x = 0;
uint8_t cls = 0; // 0 input, 1 new split (kept-kept), 2 translated
uint32_t chain = NONE; // translated: smallest chain among its translated ends
};
struct Model {
std::vector<uint32_t> first; // per site node: first piece (size N + 1)
std::vector<MPiece> pieces;
std::vector<MLink> links;
std::unordered_set<uint64_t> keys;
std::vector<uint8_t> changed; // per site node: cut or zipped
FateMap fates;
std::string error; // internal inconsistency (never expected)
uint32_t npieces(uint32_t i) const { return first[i + 1] - first[i]; }
uint32_t piece_ending(uint32_t i, int64_t x) const {
uint32_t a = first[i], b = first[i + 1];
while (a < b) { // first piece with end >= x
uint32_t m = (a + b) / 2;
if (pieces[m].off + pieces[m].len < x) a = m + 1; else b = m;
}
if (a < first[i + 1] && pieces[a].off + pieces[a].len == x) return a;
return NONE;
}
uint32_t piece_starting(uint32_t i, int64_t x) const {
uint32_t a = first[i], b = first[i + 1];
while (a < b) {
uint32_t m = (a + b) / 2;
if (pieces[m].off < x) a = m + 1; else b = m;
}
if (a < first[i + 1] && pieces[a].off == x) return a;
return NONE;
}
static uint64_t key(uint32_t sa, uint32_t sb) {
uint32_t lo = std::min(sa, sb), hi = std::max(sa, sb);
return ((uint64_t)lo << 32) | hi;
}
bool has_link(uint32_t sa, uint32_t sb) const { return keys.count(key(sa, sb)) != 0; }
};
struct RawLink {
End a, b;
int32_t sr = -1;
uint32_t src = NONE;
uint32_t split_i = NONE;
int64_t split_x = 0;
uint8_t cls = 0;
uint32_t chain = NONE;
};
// Build the edited site from fates and extra (block-end) cut points per space. drop_chain
// (debug, Z29): the first translated link of that chain is left out.
void build_model(const Ctx& cx, const FateMap& fates, const std::vector<std::set<int64_t>>& space_cuts, uint32_t drop_chain,
Model& m) {
const Graph& g = cx.g;
const uint32_t N = (uint32_t)cx.nodes.size();
m = Model();
m.fates = fates;
std::unordered_map<uint32_t, std::vector<int64_t>> cuts;
for (const auto& kv : fates) {
int64_t L = cx.len(kv.first);
for (const Fate& f : kv.second) {
if (f.a > 0) cuts[kv.first].push_back(f.a);
if (f.b < L) cuts[kv.first].push_back(f.b);
}
}
auto tr = [&](const End& e, uint32_t& chain) -> End {
if (e.i == NONE) return e;
auto it = fates.find(e.i);
if (it == fates.end()) return e;
const Fate* f = e.right ? fate_ending(it->second, e.x) : fate_starting(it->second, e.x);
if (!f) return e;
chain = std::min(chain, f->chain);
return translate(cx, *f, e.right);
};
std::vector<RawLink> raw;
raw.reserve(cx.links.size() + 16);
for (uint32_t li : cx.links) {
const Link& l = g.links[li];
RawLink r;
uint32_t ch = NONE;
r.a = tr(side_end(cx, l.a), ch);
r.b = tr(side_end(cx, l.b), ch);
r.sr = l.sr;
r.src = li;
r.cls = ch == NONE ? 0 : 2;
r.chain = ch;
if (r.a.i == NONE || r.b.i == NONE) { m.error = strf("link %u leaves the site", li); return; }
raw.push_back(r);
}
for (const auto& kv : fates) {
std::vector<int64_t> xs;
int64_t L = cx.len(kv.first);
for (const Fate& f : kv.second) {
if (f.a > 0) xs.push_back(f.a);
if (f.b < L) xs.push_back(f.b);
}
std::sort(xs.begin(), xs.end());
xs.erase(std::unique(xs.begin(), xs.end()), xs.end());
for (int64_t x : xs) {
RawLink r;
uint32_t ch = NONE;
End er, el;
er.i = el.i = kv.first;
er.x = el.x = x;
er.right = true;
el.right = false;
r.a = tr(er, ch);
r.b = tr(el, ch);
r.sr = g.nodes[cx.nodes[kv.first]].sr;
r.split_i = kv.first;
r.split_x = x;
r.cls = 2;
r.chain = ch;
raw.push_back(r);
}
}
// own adjacency inside a node: dropped, no cut
auto own_adj = [&](const RawLink& r) {
return r.a.i == r.b.i && r.a.x == r.b.x && r.a.right != r.b.right && r.a.x > 0 && r.a.x < cx.len(r.a.i);
};
for (const RawLink& r : raw) {
if (own_adj(r)) continue;
if (r.a.x > 0 && r.a.x < cx.len(r.a.i)) cuts[r.a.i].push_back(r.a.x);
if (r.b.x > 0 && r.b.x < cx.len(r.b.i)) cuts[r.b.i].push_back(r.b.x);
}
// P of every block end
for (size_t sp = 0; sp < space_cuts.size() && sp < cx.spaces.size(); ++sp) {
const TSpace& S = cx.spaces[sp];
for (int64_t T : space_cuts[sp]) {
if (T <= S.lo() || T >= S.hi()) continue;
size_t k = S.elem_starting(T);
if (T == S.c[k]) continue;
Handle h = S.walk[k];
uint32_t i = cx.idx(handle_node(h));
int64_t d = T - S.c[k];
cuts[i].push_back(handle_rev(h) ? cx.len(i) - d : d);
}
}
// pieces
m.first.assign(N + 1, 0);
m.changed.assign(N, 0);
for (uint32_t i = 0; i < N; ++i) {
m.first[i] = (uint32_t)m.pieces.size();
int64_t L = cx.len(i);
std::vector<int64_t> xs;
auto it = cuts.find(i);
if (it != cuts.end()) {
xs = it->second;
std::sort(xs.begin(), xs.end());
xs.erase(std::unique(xs.begin(), xs.end()), xs.end());
}
int64_t p = 0;
auto fit = fates.find(i);
for (size_t k = 0; k <= xs.size(); ++k) {
int64_t q = k < xs.size() ? xs[k] : L;
if (q <= p || q > L) continue;
MPiece mp;
mp.i = i;
mp.off = p;
mp.len = q - p;
if (fit != fates.end()) {
for (const Fate& f : fit->second) {
if (f.a == p && f.b == q) { mp.kept = false; mp.chain = f.chain; break; }
if (f.a < q && p < f.b)
m.error = strf("a cut at %lld..%lld falls inside a zipped piece of %s", (long long)p, (long long)q,
g.name(cx.nodes[i]).c_str());
}
}
m.pieces.push_back(mp);
p = q;
}
if (m.pieces.size() - m.first[i] > 1 || fit != fates.end()) m.changed[i] = 1;
}
m.first[N] = (uint32_t)m.pieces.size();
if (!m.error.empty()) return;
auto resolve = [&](const End& e, uint32_t& side) -> bool {
uint32_t p = e.right ? m.piece_ending(e.i, e.x) : m.piece_starting(e.i, e.x);
if (p == NONE || !m.pieces[p].kept) return false;
side = 2 * p + (e.right ? 1u : 0u);
return true;
};
// links in priority order: input (no translated end), new split links, translated
std::vector<MLink> cand;
cand.reserve(raw.size() + 16);
std::vector<MLink> translated;
for (const RawLink& r : raw) {
if (own_adj(r)) continue;
MLink l;
if (!resolve(r.a, l.sa) || !resolve(r.b, l.sb)) {
const End& e = r.a;
m.error = strf("a link end resolves to no live piece (%s %lld %s)", g.name(cx.nodes[e.i]).c_str(), (long long)e.x,
e.right ? "R" : "L");
return;
}
l.sr = r.sr;
l.src = r.src;
l.split_i = r.split_i;
l.split_x = r.split_x;
l.cls = r.cls;
l.chain = r.chain;
if (r.cls == 0) cand.push_back(l);
else translated.push_back(l);
}
for (uint32_t i = 0; i < N; ++i) {
if (fates.count(i)) continue;
for (uint32_t p = m.first[i]; p + 1 < m.first[i + 1]; ++p) {
MLink l;
l.sa = 2 * p + 1;
l.sb = 2 * (p + 1);
l.sr = g.nodes[cx.nodes[i]].sr;
l.split_i = i;
l.split_x = m.pieces[p + 1].off;
l.cls = 1;
cand.push_back(l);
}
}
std::stable_sort(translated.begin(), translated.end(), [&](const MLink& x, const MLink& y) {
if (x.src != y.src) return x.src < y.src; // translated input links first (NONE last)
if (x.split_i != y.split_i) return cx.nodes[x.split_i] < cx.nodes[y.split_i];
return x.split_x < y.split_x;
});
bool dropped_debug = false;
for (MLink& l : translated) {
if (!dropped_debug && drop_chain != NONE && l.chain == drop_chain) { dropped_debug = true; continue; }
cand.push_back(l);
}
m.links.reserve(cand.size());
for (MLink& l : cand) {
if (!m.keys.insert(Model::key(l.sa, l.sb)).second) continue;
m.links.push_back(l);
}
}
// ================================================================ forward-strand SCCs
// Non-trivial SCCs of the forward-strand graph of the model (reference '+', alt pieces as their
// node's creator excursion walks it, inversion-type links ignored), each as the sorted set of the
// site nodes it involves.
std::vector<std::vector<uint32_t>> forward_sccs(const Ctx& cx, const Model& m) {
const uint32_t P = (uint32_t)m.pieces.size();
std::vector<uint32_t> off(P + 1, 0);
std::vector<std::pair<uint32_t, uint32_t>> edges;
edges.reserve(m.links.size());
std::vector<uint8_t> selfloop(P, 0);
for (const MLink& l : m.links) {
uint32_t pa = l.sa >> 1, pb = l.sb >> 1;
bool ra = (l.sa & 1u) != 0, rb = (l.sb & 1u) != 0;
bool fa = cx.orient_rev[m.pieces[pa].i] != 0, fb = cx.orient_rev[m.pieces[pb].i] != 0;
// the exit side of a '+' piece is R, its entry side L ('-': the reverse)
bool ab = ra == !fa && rb == fb;
bool ba = rb == !fb && ra == fa;
if (ab) edges.push_back(std::make_pair(pa, pb));
if (ba && !(ab && pa == pb)) edges.push_back(std::make_pair(pb, pa));
if ((ab || ba) && pa == pb) selfloop[pa] = 1;
}
for (const auto& e : edges) off[e.first + 1]++;
for (uint32_t i = 0; i < P; ++i) off[i + 1] += off[i];
std::vector<uint32_t> adj(edges.size());
{
std::vector<uint32_t> fill(off.begin(), off.end() - 1);
for (const auto& e : edges) adj[fill[e.first]++] = e.second;
}
std::vector<uint32_t> index(P, NONE), low(P, 0);
std::vector<uint8_t> onst(P, 0);
std::vector<uint32_t> st;
struct Fr { uint32_t v, ei; };
std::vector<Fr> cs;
uint32_t idx = 0;
std::vector<std::vector<uint32_t>> out;
for (uint32_t s0 = 0; s0 < P; ++s0) {
if (index[s0] != NONE || !m.pieces[s0].kept) continue;
index[s0] = low[s0] = idx++;
st.push_back(s0);
onst[s0] = 1;
cs.push_back(Fr{s0, off[s0]});
while (!cs.empty()) {
uint32_t v = cs.back().v;
if (cs.back().ei < off[v + 1]) {
uint32_t w = adj[cs.back().ei++];
if (index[w] == NONE) {
index[w] = low[w] = idx++;
st.push_back(w);
onst[w] = 1;
cs.push_back(Fr{w, off[w]});
} else if (onst[w]) {
low[v] = std::min(low[v], index[w]);
}
} else {
if (low[v] == index[v]) {
std::vector<uint32_t> members;
while (true) {
uint32_t w = st.back();
st.pop_back();
onst[w] = 0;
members.push_back(w);
if (w == v) break;
}
if (members.size() > 1 || selfloop[members[0]]) {
std::vector<uint32_t> parents;
for (uint32_t p : members) parents.push_back(m.pieces[p].i);
std::sort(parents.begin(), parents.end());
parents.erase(std::unique(parents.begin(), parents.end()), parents.end());
out.push_back(std::move(parents));
}
}
cs.pop_back();
if (!cs.empty()) {
uint32_t u = cs.back().v;
low[u] = std::min(low[u], low[v]);
}
}
}
}
std::sort(out.begin(), out.end());
return out;
}
// an SCC of `after` whose node set is not inside one SCC of `before`
bool new_scc(const std::vector<std::vector<uint32_t>>& after, const std::vector<std::vector<uint32_t>>& before) {
for (const auto& a : after) {
bool inside = false;
for (const auto& b : before)
if (std::includes(b.begin(), b.end(), a.begin(), a.end())) { inside = true; break; }
if (!inside) return true;
}
return false;
}
// rank-0 back edges: a link joining the R side of reference piece r to the L side of reference
// piece l with start(l) < end(r)
uint64_t ref_back_edges(const Ctx& cx, const Model& m) {
uint64_t n = 0;
for (const MLink& l : m.links) {
uint32_t pa = l.sa >> 1, pb = l.sb >> 1;
bool ra = (l.sa & 1u) != 0, rb = (l.sb & 1u) != 0;
const MPiece& A = m.pieces[pa];
const MPiece& B = m.pieces[pb];
if (!cx.is_ref[A.i] || !cx.is_ref[B.i] || ra == rb) continue;
const MPiece& R = ra ? A : B;
const MPiece& L = ra ? B : A;
int64_t r_end = cx.g.start(cx.nodes[R.i]) + R.off + R.len;
int64_t l_start = cx.g.start(cx.nodes[L.i]) + L.off;
if (l_start < r_end) ++n;
}
return n;
}
// ================================================================ chains, pieces
enum PState : uint8_t { P_NEW = 0, P_COMPAT = 1, P_DROP = 2 };
struct XPiece {
uint32_t part = 0, wpos = 0;
uint32_t i = NONE; // site node index of the query node
bool wrev = false; // walked '-'
int64_t a = 0, b = 0; // node-forward
int64_t qa = 0, qb = 0; // walk
int64_t ta = 0, tb = 0;
char rel = '+';
char strand = '+'; // its part's strand
int64_t aligned = 0; // '=' and 'X' columns
uint8_t state = P_NEW;
std::string why; // drop rule
Fate img; // P_COMPAT: the accepted image
};
struct Cand {
bool alt = false;
uint32_t unit = NONE; // reference: unit id; alt: AltCandidate::key
uint32_t space = 0;
const Excursion* exc = nullptr;
std::vector<Handle> walk; // query walk (alt handles, walk orientation)
std::vector<int64_t> off; // walk offsets (size walk + 1)
int64_t wlo = 0, whi = 0; // window in space coordinates
std::vector<ChainPart> parts;
std::vector<XPiece> base; // pieces after trimming and snapping (acceptance starts from these)
int64_t score = 0, order_bp = 0;
int64_t order_w = 0; // the order key: kept bp weighted by node support
uint32_t weight = 0;
ReportRow* row = nullptr;
bool drop_link = false; // debug
// outcome of the last acceptance
std::string outcome;
int64_t kept_bp = -1, new_bp = 0;
std::vector<std::string> dropped;
std::vector<XPiece> final_pieces;
};
struct State {
FateMap fates;
std::vector<std::set<int64_t>> space_cuts;
std::vector<std::vector<uint32_t>> sccs; // forward SCCs of the current edit
std::vector<uint32_t> accepted; // candidate indices, acceptance order
};
// '=' and 'X' columns whose query base lies in walk interval [qa, qb) of part p
int64_t aligned_cols(const ChainPart& p, int64_t qa, int64_t qb) {
int64_t c0, c1;
if (p.strand == '+') { c0 = qa - p.qs; c1 = qb - p.qs; }
else { c0 = p.qe - qb; c1 = p.qe - qa; }
int64_t q = 0, n = 0;
for (const CigarOp& op : p.ops) {
if (q >= c1) break;
if (op.op == '=' || op.op == 'X' || op.op == 'M') {
int64_t a = std::max(q, c0), b = std::min(q + (int64_t)op.len, c1);
if (b > a) n += b - a;
q += op.len;
} else if (op.op == 'I') {
q += op.len;
}
}
return n;
}
// target point at a piece's walk start / walk end
int64_t wstart_t(const XPiece& p) { return p.strand == '+' ? p.ta : p.tb; }
int64_t wend_t(const XPiece& p) { return p.strand == '+' ? p.tb : p.ta; }
void set_wstart_t(XPiece& p, int64_t t) { if (p.strand == '+') p.ta = t; else p.tb = t; }
void set_wend_t(XPiece& p, int64_t t) { if (p.strand == '+') p.tb = t; else p.ta = t; }
std::string piece_label(const Ctx& cx, uint32_t i, int64_t a, int64_t b) {
return strf("%s[%lld-%lld)", cx.g.name(cx.nodes[i]).c_str(), (long long)a, (long long)b);
}
// pieces of part j of c over walk interval [x0, x1): spec 6.1 and 6.2's projection
void part_pieces(const Ctx& cx, const Cand& c, uint32_t j, int64_t x0, int64_t x1, std::vector<XPiece>& out) {
const ChainPart& p = c.parts[j];
if (x1 <= x0) return;
size_t k = (size_t)(std::upper_bound(c.off.begin(), c.off.end(), x0) - c.off.begin());
k = k == 0 ? 0 : k - 1;
for (; k < c.walk.size() && c.off[k] < x1; ++k) {
int64_t L = c.off[k + 1] - c.off[k];
int64_t qa = std::max(c.off[k], x0), qb = std::min(c.off[k] + L, x1);
if (qb <= qa) continue;
XPiece x;
x.part = j;
x.wpos = (uint32_t)k;
x.i = cx.idx(handle_node(c.walk[k]));
x.wrev = handle_rev(c.walk[k]);
x.qa = qa;
x.qb = qb;
if (!x.wrev) { x.a = qa - c.off[k]; x.b = qb - c.off[k]; }
else { x.a = L - (qb - c.off[k]); x.b = L - (qa - c.off[k]); }
x.strand = p.strand;
if (p.strand == '+') { x.ta = project_left_snap(p, qa); x.tb = project_left_snap(p, qb); }
else { x.ta = project_left_snap(p, qb); x.tb = project_left_snap(p, qa); }
x.rel = (x.wrev == (p.strand == '-')) ? '+' : '-';
x.aligned = aligned_cols(p, qa, qb);
out.push_back(x);
}
}
// smallest x in [lo, hi] with pred(P(x)), pred false then true along x (P is monotone); hi + 1 if none
template <typename F>
int64_t first_x(const ChainPart& p, int64_t lo, int64_t hi, F pred) {
int64_t a = lo, b = hi + 1;
while (a < b) {
int64_t m = a + (b - a) / 2;
if (pred(project_left_snap(p, m))) b = m; else a = m + 1;
}
return a;
}
// largest x in [lo, hi] with pred(P(x)), pred true then false along x; lo - 1 if none
template <typename F>
int64_t last_x(const ChainPart& p, int64_t lo, int64_t hi, F pred) {
int64_t a = lo, b = hi + 1;
while (a < b) {
int64_t m = a + (b - a) / 2;
if (pred(project_left_snap(p, m))) a = m + 1; else b = m;
}
return a - 1;
}
// Spec 6.1-6.3 for one chain: pieces (node-forward interval, target, rel) of every part, with the
// query/target overlaps that the chain's overlap tolerance G allows between parts trimmed from the
// later part, free block ends snapped, and folds inside the chain dropped.
//
// Overlap trimming. The query overlap goes first: a part starts after every earlier part's
// (trimmed) query end. Then its target must not overlap the target of any earlier part. The
// chain DP lets two parts overlap by up to G at either end of the later one: at its query start
// when it continues a segment (same strand, collinear), but at its query END when it switches
// strand above the previous segment as '-' (frame F: A rc(B), a '+' record running into the
// breakpoint's microhomology) or below it as '+' (frame R), or when a part of a '-' segment
// touches the segment's floor. So the overlap is cut from whichever end of the later part's
// target it lies at, by moving the part's query start or end (P is monotone). A part left with
// nothing is a fold: its pieces are reported as dropped "(fold)" rather than lost silently.
void prepare_chain(const Ctx& cx, Cand& c, const std::vector<int64_t>& boundaries) {
const int64_t G = cx.opt.G;
const uint32_t np = (uint32_t)c.parts.size();
std::vector<int64_t> x0(np, 0), x1(np, 0), hlo(np, 0), hhi(np, 0);
std::vector<uint8_t> live(np, 0);
std::vector<XPiece> folded; // pieces of parts that vanished (reported, never zipped)
int64_t qend = INT64_MIN; // largest query end of the earlier live parts
for (uint32_t j = 0; j < np; ++j) {
const ChainPart& p = c.parts[j];
const bool fwd = p.strand == '+';
int64_t a = std::max<int64_t>(p.qs, 0), b = p.qe;
a = std::max(a, qend); // query overlap
if (a >= b) continue; // all of its query is in earlier parts
const int64_t qa = a;
bool gone = false;
{
int64_t lo = project_left_snap(p, fwd ? a : b), hi = project_left_snap(p, fwd ? b : a);
for (uint32_t k = 0; k < j && !gone; ++k) {
if (!live[k] || hhi[k] <= lo || hlo[k] >= hi) continue;
bool bottom = hlo[k] <= lo, top = hhi[k] >= hi;
if (bottom && top) { gone = true; break; } // the earlier part covers this one's target
if (!bottom && !top) bottom = hhi[k] - lo <= hi - hlo[k]; // strictly inside: the cheaper end
if (bottom) lo = hhi[k];
else hi = hlo[k];
if (lo >= hi) gone = true;
}
if (!gone) {
// [lo, hi) back to the query: '+' targets grow with x, '-' targets shrink
if (fwd) {
a = first_x(p, a, b, [&](int64_t t) { return t >= lo; });
b = last_x(p, a, b, [&](int64_t t) { return t <= hi; });
} else {
a = first_x(p, a, b, [&](int64_t t) { return t <= hi; });
b = last_x(p, a, b, [&](int64_t t) { return t >= lo; });
}
gone = a >= b;
}
}
if (gone) {
part_pieces(cx, c, j, qa, p.qe, folded); // its query beyond the earlier parts stays alt
continue;
}
x0[j] = a;
x1[j] = b;
int64_t ta = project_left_snap(p, a), tb = project_left_snap(p, b);
hlo[j] = std::min(ta, tb);
hhi[j] = std::max(ta, tb);
live[j] = 1;
qend = std::max(qend, b);
}
for (XPiece& x : folded) {
x.state = P_DROP;
x.why = "(fold)";
}
std::vector<XPiece> pcs;
std::vector<std::pair<size_t, size_t>> range(np, std::make_pair((size_t)0, (size_t)0));
for (uint32_t j = 0; j < np; ++j) {
size_t b0 = pcs.size();
if (live[j]) part_pieces(cx, c, j, x0[j], x1[j], pcs);
range[j] = std::make_pair(b0, pcs.size());
}
// a partial node piece shorter than G stays alt: differences shorter than G are absorbed
for (XPiece& p : pcs)
if (p.b - p.a < G && p.b - p.a < cx.len(p.i)) { p.state = P_DROP; p.why = "(short)"; }
auto first_live = [&](uint32_t j) -> size_t {
for (size_t k = range[j].first; k < range[j].second; ++k)
if (pcs[k].state != P_DROP) return k;
return SIZE_MAX;
};
auto last_live = [&](uint32_t j) -> size_t {
for (size_t k = range[j].second; k-- > range[j].first;)
if (pcs[k].state != P_DROP) return k;
return SIZE_MAX;
};
auto hull_of = [&](uint32_t j, int64_t& lo, int64_t& hi) {
lo = INT64_MAX;
hi = INT64_MIN;
for (size_t k = range[j].first; k < range[j].second; ++k) {
if (pcs[k].state == P_DROP) continue;
lo = std::min(lo, pcs[k].ta);
hi = std::max(hi, pcs[k].tb);
}
};
// snap free block ends (spec 6.2): within G of a window end or an existing node boundary of
// the target space, and the move shorter than the end piece's target; never at a junction
// shared with another part, never into another part's target
for (uint32_t j = 0; j < np; ++j) {
if (first_live(j) == SIZE_MAX) continue;
for (int end = 0; end < 2; ++end) {
size_t ek = end == 0 ? first_live(j) : last_live(j);
int64_t T = end == 0 ? wstart_t(pcs[ek]) : wend_t(pcs[ek]);
bool shared = false;
for (uint32_t k = 0; k < np; ++k) {
if (k == j || first_live(k) == SIZE_MAX) continue;
if (wstart_t(pcs[first_live(k)]) == T || wend_t(pcs[last_live(k)]) == T) shared = true;
}
if (shared) continue;
int64_t tlen = pcs[ek].tb - pcs[ek].ta;
int64_t best = T, bestd = INT64_MAX;
auto consider = [&](int64_t B) {
if (B < c.wlo || B > c.whi) return;
int64_t d = iabs64(B - T);
if (d == 0 || d > G || d >= tlen) return;
if (d < bestd || (d == bestd && B < best)) { bestd = d; best = B; }
};
consider(c.wlo);
consider(c.whi);
for (auto it = std::lower_bound(boundaries.begin(), boundaries.end(), T - G); it != boundaries.end() && *it <= T + G; ++it)
consider(*it);
if (bestd == INT64_MAX) continue;
XPiece trial = pcs[ek];
if (end == 0) set_wstart_t(trial, best); else set_wend_t(trial, best);
if (trial.tb <= trial.ta) continue;
int64_t lo = INT64_MAX, hi = INT64_MIN;
for (size_t k = range[j].first; k < range[j].second; ++k) {
if (pcs[k].state == P_DROP) continue;
const XPiece& z = k == ek ? trial : pcs[k];
lo = std::min(lo, z.ta);
hi = std::max(hi, z.tb);
}
bool clash = false;
for (uint32_t k = 0; k < np; ++k) {
if (k == j || first_live(k) == SIZE_MAX) continue;
int64_t l2, h2;
hull_of(k, l2, h2);
if (std::max(lo, l2) < std::min(hi, h2)) clash = true;
}
if (!clash) pcs[ek] = trial;
}
}
// folds inside the chain: a piece whose target overlaps an earlier piece of another part
for (size_t k = 0; k < pcs.size(); ++k) {
if (pcs[k].state == P_DROP) continue;
for (size_t k2 = 0; k2 < k; ++k2) {
if (pcs[k2].part == pcs[k].part || pcs[k2].state == P_DROP) continue;
if (std::max(pcs[k].ta, pcs[k2].ta) < std::min(pcs[k].tb, pcs[k2].tb)) {
pcs[k].state = P_DROP;
pcs[k].why = "(fold)";
break;
}
}
}
// the parts that vanished, after every live piece (so that piece order still gives junctions)
pcs.insert(pcs.end(), folded.begin(), folded.end());
c.base = std::move(pcs);
c.score = 0;
for (const ChainPart& p : c.parts) c.score += p.n_eq;
}
// junction neighbours: consecutive pieces of one part, or of two parts whose junction lands on
// one target point
bool junction(const XPiece& x, const XPiece& y) {
if (x.qb != y.qa) return false;
if (x.part == y.part) return true;
return wend_t(x) == wstart_t(y);
}
// ================================================================ images (V2, V3, --check)
struct ImgEl {
bool run = false;
uint32_t piece = NONE; // !run
bool rev = false;
uint32_t space = 0; // run: [t0, t1) of space, walked reversed if rev
int64_t t0 = 0, t1 = 0;
};
void append_oriented(std::string& out, const std::string& s, int64_t off, int64_t len, bool rev) {
if (!rev) { out.append(s, (size_t)off, (size_t)len); return; }
for (int64_t k = off + len; k-- > off;) out.push_back(comp_base(s[(size_t)k]));
}
// pieces of the space covering [t0, t1) in space order ('+' run); false if a boundary is missing
bool space_pieces(const Ctx& cx, const Model& m, const TSpace& S, int64_t t0, int64_t t1, std::vector<std::pair<uint32_t, bool>>& out,
std::string& why) {
if (t1 <= t0) return true;
size_t k = S.elem_starting(t0);
for (; k + 1 < S.c.size() && S.c[k] < t1; ++k) {
int64_t s0 = std::max(t0, S.c[k]), s1 = std::min(t1, S.c[k + 1]);
if (s1 <= s0) continue;
Handle h = S.walk[k];
uint32_t i = cx.idx(handle_node(h));
int64_t L = cx.len(i);
int64_t na, nb;
if (!handle_rev(h)) { na = s0 - S.c[k]; nb = s1 - S.c[k]; }
else { na = L - (s1 - S.c[k]); nb = L - (s0 - S.c[k]); }
uint32_t pa = m.piece_starting(i, na), pb = m.piece_ending(i, nb);
if (pa == NONE || pb == NONE || pb < pa) {
why = strf("no piece boundary of %s at %lld/%lld (target %lld-%lld)", cx.g.name(cx.nodes[i]).c_str(), (long long)na, (long long)nb,
(long long)t0, (long long)t1);
return false;
}
if (!handle_rev(h))
for (uint32_t p = pa; p <= pb; ++p) out.push_back(std::make_pair(p, false));
else
for (uint32_t p = pb + 1; p-- > pa;) out.push_back(std::make_pair(p, true));
}
return true;
}
// the input sequence of [t0, t1) of a space
void space_seq(const Ctx& cx, const TSpace& S, int64_t t0, int64_t t1, std::string& out) {
if (t1 <= t0) return;
size_t k = S.elem_starting(t0);
for (; k + 1 < S.c.size() && S.c[k] < t1; ++k) {
int64_t s0 = std::max(t0, S.c[k]), s1 = std::min(t1, S.c[k + 1]);
if (s1 <= s0) continue;
Handle h = S.walk[k];
const std::string& seq = cx.g.nodes[handle_node(h)].seq;
int64_t L = (int64_t)seq.size();
if (!handle_rev(h)) append_oriented(out, seq, s0 - S.c[k], s1 - s0, false);
else append_oriented(out, seq, L - (s1 - S.c[k]), s1 - s0, true);
}
}
// The image of an input walk (anchors included) in the model. Returns "" when it exists link by
// link, spells the predicted sequence (kept pieces as they are, zipped pieces as their targets
// read from the input) and reaches no target piece twice; else "V2" or "V3" with a message.
std::string check_image(const Ctx& cx, const Model& m, const std::vector<Handle>& walk, std::string& msg) {
std::vector<ImgEl> els;
for (Handle h : walk) {
uint32_t i = cx.idx(handle_node(h));
if (i == NONE) { msg = "walk leaves the site at " + cx.g.handle_str(h); return "V2"; }
bool orev = handle_rev(h);
uint32_t n = m.npieces(i);
auto fit = m.fates.find(i);
for (uint32_t k = 0; k < n; ++k) {
uint32_t p = orev ? m.first[i] + n - 1 - k : m.first[i] + k;
const MPiece& mp = m.pieces[p];
ImgEl e;
if (mp.kept) {
e.piece = p;
e.rev = orev;
} else {
const Fate* f = fit == m.fates.end() ? nullptr : fate_starting(fit->second, mp.off);
if (!f || f->b != mp.off + mp.len) { msg = "zipped piece without a fate"; return "V2"; }
e.run = true;
e.space = f->space;
e.t0 = f->ta;
e.t1 = f->tb;
e.rev = (f->rel == '-') != orev;