21139: Reduce memory usage in tiling.
[lightning.git] / tilelib.go
1 // Copyright (C) The Lightning Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package lightning
6
7 import (
8         "bufio"
9         "bytes"
10         "context"
11         "encoding/gob"
12         "fmt"
13         "io"
14         "os"
15         "regexp"
16         "runtime"
17         "sort"
18         "strings"
19         "sync"
20         "sync/atomic"
21
22         "github.com/klauspost/pgzip"
23         log "github.com/sirupsen/logrus"
24         "golang.org/x/crypto/blake2b"
25 )
26
27 type tileVariantID uint16 // 1-based
28
29 type tileLibRef struct {
30         Tag     tagID
31         Variant tileVariantID
32 }
33
34 type tileSeq map[string][]tileLibRef
35
36 func (tseq tileSeq) Variants() ([]tileVariantID, int, int) {
37         maxtag := 0
38         for _, refs := range tseq {
39                 for _, ref := range refs {
40                         if maxtag < int(ref.Tag) {
41                                 maxtag = int(ref.Tag)
42                         }
43                 }
44         }
45         vars := make([]tileVariantID, maxtag+1)
46         var kept, dropped int
47         for _, refs := range tseq {
48                 for _, ref := range refs {
49                         if vars[int(ref.Tag)] != 0 {
50                                 dropped++
51                         } else {
52                                 kept++
53                         }
54                         vars[int(ref.Tag)] = ref.Variant
55                 }
56         }
57         return vars, kept, dropped
58 }
59
60 type tileLibrary struct {
61         retainNoCalls       bool
62         skipOOO             bool
63         retainTileSequences bool
64         useDups             bool
65
66         taglib         *tagLibrary
67         variant        [][][blake2b.Size256]byte
68         refseqs        map[string]map[string][]tileLibRef
69         compactGenomes map[string][]tileVariantID
70         seq2           map[[2]byte]map[[blake2b.Size256]byte][]byte
71         seq2lock       map[[2]byte]sync.Locker
72         variants       int64
73         // if non-nil, write out any tile variants added while tiling
74         encoder *gob.Encoder
75         // set Ref flag when writing new variants to encoder
76         encodeRef bool
77
78         onAddTileVariant func(libref tileLibRef, hash [blake2b.Size256]byte, seq []byte) error
79         onAddGenome      func(CompactGenome) error
80         onAddRefseq      func(CompactSequence) error
81
82         mtx   sync.RWMutex
83         vlock []sync.Locker
84 }
85
86 func (tilelib *tileLibrary) loadTagSet(newtagset [][]byte) error {
87         // Loading a tagset means either passing it through to the
88         // output (if it's the first one we've seen), or just ensuring
89         // it doesn't disagree with what we already have.
90         if len(newtagset) == 0 {
91                 return nil
92         }
93         tilelib.mtx.Lock()
94         defer tilelib.mtx.Unlock()
95         if tilelib.taglib == nil || tilelib.taglib.Len() == 0 {
96                 tilelib.taglib = &tagLibrary{}
97                 err := tilelib.taglib.setTags(newtagset)
98                 if err != nil {
99                         return err
100                 }
101                 if tilelib.encoder != nil {
102                         err = tilelib.encoder.Encode(LibraryEntry{
103                                 TagSet: newtagset,
104                         })
105                         if err != nil {
106                                 return err
107                         }
108                 }
109         } else if tilelib.taglib.Len() != len(newtagset) {
110                 return fmt.Errorf("cannot merge libraries with differing tagsets")
111         } else {
112                 current := tilelib.taglib.Tags()
113                 for i := range newtagset {
114                         if !bytes.Equal(newtagset[i], current[i]) {
115                                 return fmt.Errorf("cannot merge libraries with differing tagsets")
116                         }
117                 }
118         }
119         return nil
120 }
121
122 func (tilelib *tileLibrary) loadTileVariants(tvs []TileVariant, variantmap map[tileLibRef]tileVariantID) error {
123         for _, tv := range tvs {
124                 // Assign a new variant ID (unique across all inputs)
125                 // for each input variant.
126                 variantmap[tileLibRef{Tag: tv.Tag, Variant: tv.Variant}] = tilelib.getRef(tv.Tag, tv.Sequence, tv.Ref).Variant
127         }
128         return nil
129 }
130
131 func (tilelib *tileLibrary) loadCompactGenomes(cgs []CompactGenome, variantmap map[tileLibRef]tileVariantID) error {
132         log.Debugf("loadCompactGenomes: %d", len(cgs))
133         var wg sync.WaitGroup
134         errs := make(chan error, 1)
135         for _, cg := range cgs {
136                 wg.Add(1)
137                 cg := cg
138                 go func() {
139                         defer wg.Done()
140                         for i, variant := range cg.Variants {
141                                 if len(errs) > 0 {
142                                         return
143                                 }
144                                 if variant == 0 {
145                                         continue
146                                 }
147                                 tag := tagID(i / 2)
148                                 newvariant, ok := variantmap[tileLibRef{Tag: tag, Variant: variant}]
149                                 if !ok {
150                                         err := fmt.Errorf("oops: genome %q has variant %d for tag %d, but that variant was not in its library", cg.Name, variant, tag)
151                                         select {
152                                         case errs <- err:
153                                         default:
154                                         }
155                                         return
156                                 }
157                                 // log.Tracef("loadCompactGenomes: cg %s tag %d variant %d => %d", cg.Name, tag, variant, newvariant)
158                                 cg.Variants[i] = newvariant
159                         }
160                         if tilelib.onAddGenome != nil {
161                                 err := tilelib.onAddGenome(cg)
162                                 if err != nil {
163                                         select {
164                                         case errs <- err:
165                                         default:
166                                         }
167                                         return
168                                 }
169                         }
170                         if tilelib.encoder != nil {
171                                 err := tilelib.encoder.Encode(LibraryEntry{
172                                         CompactGenomes: []CompactGenome{cg},
173                                 })
174                                 if err != nil {
175                                         select {
176                                         case errs <- err:
177                                         default:
178                                         }
179                                         return
180                                 }
181                         }
182                         if tilelib.compactGenomes != nil {
183                                 tilelib.mtx.Lock()
184                                 defer tilelib.mtx.Unlock()
185                                 tilelib.compactGenomes[cg.Name] = cg.Variants
186                         }
187                 }()
188         }
189         wg.Wait()
190         go close(errs)
191         return <-errs
192 }
193
194 func (tilelib *tileLibrary) loadCompactSequences(cseqs []CompactSequence, variantmap map[tileLibRef]tileVariantID) error {
195         log.Infof("loadCompactSequences: %d todo", len(cseqs))
196         for _, cseq := range cseqs {
197                 log.Infof("loadCompactSequences: checking %s", cseq.Name)
198                 for _, tseq := range cseq.TileSequences {
199                         for i, libref := range tseq {
200                                 if libref.Variant == 0 {
201                                         // No variant (e.g., import
202                                         // dropped tiles with
203                                         // no-calls) = no translation.
204                                         continue
205                                 }
206                                 v, ok := variantmap[libref]
207                                 if !ok {
208                                         return fmt.Errorf("oops: CompactSequence %q has variant %d for tag %d, but that variant was not in its library", cseq.Name, libref.Variant, libref.Tag)
209                                 }
210                                 tseq[i].Variant = v
211                         }
212                 }
213                 if tilelib.encoder != nil {
214                         if err := tilelib.encoder.Encode(LibraryEntry{
215                                 CompactSequences: []CompactSequence{cseq},
216                         }); err != nil {
217                                 return err
218                         }
219                 }
220                 if tilelib.onAddRefseq != nil {
221                         err := tilelib.onAddRefseq(cseq)
222                         if err != nil {
223                                 return err
224                         }
225                 }
226                 log.Infof("loadCompactSequences: checking %s done", cseq.Name)
227         }
228         tilelib.mtx.Lock()
229         defer tilelib.mtx.Unlock()
230         if tilelib.refseqs == nil {
231                 tilelib.refseqs = map[string]map[string][]tileLibRef{}
232         }
233         for _, cseq := range cseqs {
234                 tilelib.refseqs[cseq.Name] = cseq.TileSequences
235         }
236         log.Info("loadCompactSequences: done")
237         return nil
238 }
239
240 func allFiles(path string, re *regexp.Regexp) ([]string, error) {
241         var files []string
242         f, err := open(path)
243         if err != nil {
244                 return nil, err
245         }
246         defer f.Close()
247         fis, err := f.Readdir(-1)
248         if err != nil {
249                 return []string{path}, nil
250         }
251         for _, fi := range fis {
252                 if fi.Name() == "." || fi.Name() == ".." {
253                         continue
254                 } else if child := path + "/" + fi.Name(); fi.IsDir() {
255                         add, err := allFiles(child, re)
256                         if err != nil {
257                                 return nil, err
258                         }
259                         files = append(files, add...)
260                 } else if re == nil || re.MatchString(child) {
261                         files = append(files, child)
262                 }
263         }
264         sort.Strings(files)
265         return files, nil
266 }
267
268 var matchGobFile = regexp.MustCompile(`\.gob(\.gz)?$`)
269
270 func (tilelib *tileLibrary) LoadDir(ctx context.Context, path string) error {
271         log.Infof("LoadDir: walk dir %s", path)
272         files, err := allFiles(path, matchGobFile)
273         if err != nil {
274                 return err
275         }
276         ctx, cancel := context.WithCancel(ctx)
277         defer cancel()
278         var mtx sync.Mutex
279         allcgs := make([][]CompactGenome, len(files))
280         allcseqs := make([][]CompactSequence, len(files))
281         allvariantmap := map[tileLibRef]tileVariantID{}
282         errs := make(chan error, len(files))
283         log.Infof("LoadDir: read %d files", len(files))
284         for fileno, path := range files {
285                 fileno, path := fileno, path
286                 go func() {
287                         f, err := open(path)
288                         if err != nil {
289                                 errs <- err
290                                 return
291                         }
292                         defer f.Close()
293                         defer log.Infof("LoadDir: finished reading %s", path)
294
295                         var variantmap = map[tileLibRef]tileVariantID{}
296                         var cgs []CompactGenome
297                         var cseqs []CompactSequence
298                         err = DecodeLibrary(f, strings.HasSuffix(path, ".gz"), func(ent *LibraryEntry) error {
299                                 if ctx.Err() != nil {
300                                         return ctx.Err()
301                                 }
302                                 if len(ent.TagSet) > 0 {
303                                         mtx.Lock()
304                                         if tilelib.taglib == nil || tilelib.taglib.Len() != len(ent.TagSet) {
305                                                 // load first set of tags, or
306                                                 // report mismatch if 2 sets
307                                                 // have different #tags.
308                                                 if err := tilelib.loadTagSet(ent.TagSet); err != nil {
309                                                         mtx.Unlock()
310                                                         return err
311                                                 }
312                                         }
313                                         mtx.Unlock()
314                                 }
315                                 for _, tv := range ent.TileVariants {
316                                         variantmap[tileLibRef{Tag: tv.Tag, Variant: tv.Variant}] = tilelib.getRef(tv.Tag, tv.Sequence, tv.Ref).Variant
317                                 }
318                                 cgs = append(cgs, ent.CompactGenomes...)
319                                 cseqs = append(cseqs, ent.CompactSequences...)
320                                 return nil
321                         })
322                         allcgs[fileno] = cgs
323                         allcseqs[fileno] = cseqs
324                         mtx.Lock()
325                         defer mtx.Unlock()
326                         for k, v := range variantmap {
327                                 allvariantmap[k] = v
328                         }
329                         errs <- err
330                 }()
331         }
332         for range files {
333                 err := <-errs
334                 if err != nil {
335                         return err
336                 }
337         }
338
339         log.Info("LoadDir: loadCompactGenomes")
340         var flatcgs []CompactGenome
341         for _, cgs := range allcgs {
342                 flatcgs = append(flatcgs, cgs...)
343         }
344         err = tilelib.loadCompactGenomes(flatcgs, allvariantmap)
345         if err != nil {
346                 return err
347         }
348
349         log.Info("LoadDir: loadCompactSequences")
350         var flatcseqs []CompactSequence
351         for _, cseqs := range allcseqs {
352                 flatcseqs = append(flatcseqs, cseqs...)
353         }
354         err = tilelib.loadCompactSequences(flatcseqs, allvariantmap)
355         if err != nil {
356                 return err
357         }
358
359         log.Info("LoadDir done")
360         return nil
361 }
362
363 func (tilelib *tileLibrary) WriteDir(dir string) error {
364         ntilefiles := 128
365         nfiles := ntilefiles + len(tilelib.refseqs)
366         files := make([]*os.File, nfiles)
367         for i := range files {
368                 f, err := os.OpenFile(fmt.Sprintf("%s/library.%04d.gob.gz", dir, i), os.O_CREATE|os.O_WRONLY, 0666)
369                 if err != nil {
370                         return err
371                 }
372                 defer f.Close()
373                 files[i] = f
374         }
375         bufws := make([]*bufio.Writer, nfiles)
376         for i := range bufws {
377                 bufws[i] = bufio.NewWriterSize(files[i], 1<<26)
378         }
379         zws := make([]*pgzip.Writer, nfiles)
380         for i := range zws {
381                 zws[i] = pgzip.NewWriter(bufws[i])
382                 defer zws[i].Close()
383         }
384         encoders := make([]*gob.Encoder, nfiles)
385         for i := range encoders {
386                 encoders[i] = gob.NewEncoder(zws[i])
387         }
388
389         cgnames := make([]string, 0, len(tilelib.compactGenomes))
390         for name := range tilelib.compactGenomes {
391                 cgnames = append(cgnames, name)
392         }
393         sort.Strings(cgnames)
394
395         refnames := make([]string, 0, len(tilelib.refseqs))
396         for name := range tilelib.refseqs {
397                 refnames = append(refnames, name)
398         }
399         sort.Strings(refnames)
400
401         log.Infof("WriteDir: writing %d files", nfiles)
402         ctx, cancel := context.WithCancel(context.Background())
403         defer cancel()
404         errs := make(chan error, nfiles)
405         for start := range files {
406                 start := start
407                 go func() {
408                         err := encoders[start].Encode(LibraryEntry{TagSet: tilelib.taglib.Tags()})
409                         if err != nil {
410                                 errs <- err
411                                 return
412                         }
413                         if refidx := start - ntilefiles; refidx >= 0 {
414                                 // write each ref to its own file
415                                 // (they seem to load very slowly)
416                                 name := refnames[refidx]
417                                 errs <- encoders[start].Encode(LibraryEntry{CompactSequences: []CompactSequence{{
418                                         Name:          name,
419                                         TileSequences: tilelib.refseqs[name],
420                                 }}})
421                                 return
422                         }
423                         for i := start; i < len(cgnames); i += ntilefiles {
424                                 err := encoders[start].Encode(LibraryEntry{CompactGenomes: []CompactGenome{{
425                                         Name:     cgnames[i],
426                                         Variants: tilelib.compactGenomes[cgnames[i]],
427                                 }}})
428                                 if err != nil {
429                                         errs <- err
430                                         return
431                                 }
432                         }
433                         tvs := []TileVariant{}
434                         for tag := start; tag < len(tilelib.variant) && ctx.Err() == nil; tag += ntilefiles {
435                                 tvs = tvs[:0]
436                                 for idx, hash := range tilelib.variant[tag] {
437                                         tvs = append(tvs, TileVariant{
438                                                 Tag:      tagID(tag),
439                                                 Variant:  tileVariantID(idx + 1),
440                                                 Blake2b:  hash,
441                                                 Sequence: tilelib.hashSequence(hash),
442                                         })
443                                 }
444                                 err := encoders[start].Encode(LibraryEntry{TileVariants: tvs})
445                                 if err != nil {
446                                         errs <- err
447                                         return
448                                 }
449                         }
450                         errs <- nil
451                 }()
452         }
453         for range files {
454                 err := <-errs
455                 if err != nil {
456                         return err
457                 }
458         }
459         log.Info("WriteDir: flushing")
460         for i := range zws {
461                 err := zws[i].Close()
462                 if err != nil {
463                         return err
464                 }
465                 err = bufws[i].Flush()
466                 if err != nil {
467                         return err
468                 }
469                 err = files[i].Close()
470                 if err != nil {
471                         return err
472                 }
473         }
474         log.Info("WriteDir: done")
475         return nil
476 }
477
478 // Load library data from rdr. Tile variants might be renumbered in
479 // the process; in that case, genomes variants will be renumbered to
480 // match.
481 func (tilelib *tileLibrary) LoadGob(ctx context.Context, rdr io.Reader, gz bool) error {
482         cgs := []CompactGenome{}
483         cseqs := []CompactSequence{}
484         variantmap := map[tileLibRef]tileVariantID{}
485         err := DecodeLibrary(rdr, gz, func(ent *LibraryEntry) error {
486                 if ctx.Err() != nil {
487                         return ctx.Err()
488                 }
489                 if err := tilelib.loadTagSet(ent.TagSet); err != nil {
490                         return err
491                 }
492                 if err := tilelib.loadTileVariants(ent.TileVariants, variantmap); err != nil {
493                         return err
494                 }
495                 cgs = append(cgs, ent.CompactGenomes...)
496                 cseqs = append(cseqs, ent.CompactSequences...)
497                 return nil
498         })
499         if err != nil {
500                 return err
501         }
502         if ctx.Err() != nil {
503                 return ctx.Err()
504         }
505         err = tilelib.loadCompactGenomes(cgs, variantmap)
506         if err != nil {
507                 return err
508         }
509         err = tilelib.loadCompactSequences(cseqs, variantmap)
510         if err != nil {
511                 return err
512         }
513         return nil
514 }
515
516 func (tilelib *tileLibrary) dump(out io.Writer) {
517         printTV := func(tag int, variant tileVariantID) {
518                 if variant < 1 {
519                         fmt.Fprintf(out, " -")
520                 } else if tag >= len(tilelib.variant) {
521                         fmt.Fprintf(out, " (!tag=%d)", tag)
522                 } else if int(variant) > len(tilelib.variant[tag]) {
523                         fmt.Fprintf(out, " (tag=%d,!variant=%d)", tag, variant)
524                 } else {
525                         fmt.Fprintf(out, " %x", tilelib.variant[tag][variant-1][:8])
526                 }
527         }
528         for refname, refseqs := range tilelib.refseqs {
529                 for seqname, seq := range refseqs {
530                         fmt.Fprintf(out, "ref %s %s", refname, seqname)
531                         for _, libref := range seq {
532                                 printTV(int(libref.Tag), libref.Variant)
533                         }
534                         fmt.Fprintf(out, "\n")
535                 }
536         }
537         for name, cg := range tilelib.compactGenomes {
538                 fmt.Fprintf(out, "cg %s", name)
539                 for tag, variant := range cg {
540                         printTV(tag/2, variant)
541                 }
542                 fmt.Fprintf(out, "\n")
543         }
544 }
545
546 type importStats struct {
547         InputFile             string
548         InputLabel            string
549         InputLength           int
550         InputCoverage         int
551         PathLength            int
552         DroppedRepeatedTags   int
553         DroppedOutOfOrderTags int
554 }
555
556 func (tilelib *tileLibrary) TileFasta(filelabel string, rdr io.Reader, matchChromosome *regexp.Regexp, isRef bool) (tileSeq, []importStats, error) {
557         ret := tileSeq{}
558         type foundtag struct {
559                 pos   int
560                 tagid tagID
561         }
562         found := make([]foundtag, 2000000)
563         path := make([]tileLibRef, 2000000)
564         totalFoundTags := 0
565         totalPathLen := 0
566         skippedSequences := 0
567         taglen := tilelib.taglib.TagLen()
568         var stats []importStats
569
570         in := bufio.NewReader(rdr)
571 readall:
572         for {
573                 var seqlabel string
574                 // Advance to next '>', then
575                 // read seqlabel up to \r?\n
576         readseqlabel:
577                 for seqlabelStarted := false; ; {
578                         rune, _, err := in.ReadRune()
579                         if err == io.EOF {
580                                 break readall
581                         } else if err != nil {
582                                 return nil, nil, err
583                         }
584                         switch {
585                         case rune == '\r':
586                         case seqlabelStarted && rune == '\n':
587                                 break readseqlabel
588                         case seqlabelStarted:
589                                 seqlabel += string(rune)
590                         case rune == '>':
591                                 seqlabelStarted = true
592                         default:
593                         }
594                 }
595                 log.Debugf("%s %s reading fasta", filelabel, seqlabel)
596                 if !matchChromosome.MatchString(seqlabel) {
597                         skippedSequences++
598                         continue
599                 }
600                 log.Debugf("%s %s tiling", filelabel, seqlabel)
601
602                 fasta := bytes.NewBuffer(nil)
603                 found = found[:0]
604                 err := tilelib.taglib.FindAll(in, fasta, func(tagid tagID, pos, taglen int) {
605                         found = append(found, foundtag{pos: pos, tagid: tagid})
606                 })
607                 if err != nil {
608                         return nil, nil, err
609                 }
610                 totalFoundTags += len(found)
611                 if len(found) == 0 {
612                         log.Warnf("%s %s no tags found", filelabel, seqlabel)
613                 }
614
615                 droppedDup := 0
616                 if !tilelib.useDups {
617                         // Remove any tags that appeared more than once
618                         dup := map[tagID]bool{}
619                         for _, ft := range found {
620                                 _, dup[ft.tagid] = dup[ft.tagid]
621                         }
622                         dst := 0
623                         for _, ft := range found {
624                                 if !dup[ft.tagid] {
625                                         found[dst] = ft
626                                         dst++
627                                 }
628                         }
629                         droppedDup = len(found) - dst
630                         log.Infof("%s %s dropping %d non-unique tags", filelabel, seqlabel, droppedDup)
631                         found = found[:dst]
632                 }
633
634                 droppedOOO := 0
635                 if tilelib.skipOOO {
636                         keep := longestIncreasingSubsequence(len(found), func(i int) int { return int(found[i].tagid) })
637                         for i, x := range keep {
638                                 found[i] = found[x]
639                         }
640                         droppedOOO = len(found) - len(keep)
641                         log.Infof("%s %s dropping %d out-of-order tags", filelabel, seqlabel, droppedOOO)
642                         found = found[:len(keep)]
643                 }
644
645                 log.Infof("%s %s getting %d librefs", filelabel, seqlabel, len(found))
646                 throttle := &throttle{Max: runtime.NumCPU()}
647                 path = path[:len(found)]
648                 var lowquality int64
649                 for i, f := range found {
650                         i, f := i, f
651                         throttle.Acquire()
652                         go func() {
653                                 defer throttle.Release()
654                                 var startpos, endpos int
655                                 if i == 0 {
656                                         startpos = 0
657                                 } else {
658                                         startpos = f.pos
659                                 }
660                                 if i == len(found)-1 {
661                                         endpos = fasta.Len()
662                                 } else {
663                                         endpos = found[i+1].pos + taglen
664                                 }
665                                 path[i] = tilelib.getRef(f.tagid, fasta.Bytes()[startpos:endpos], isRef)
666                                 if countBases(fasta.Bytes()[startpos:endpos]) != endpos-startpos {
667                                         atomic.AddInt64(&lowquality, 1)
668                                 }
669                         }()
670                 }
671                 throttle.Wait()
672
673                 log.Infof("%s %s copying path", filelabel, seqlabel)
674
675                 pathcopy := make([]tileLibRef, len(path))
676                 copy(pathcopy, path)
677                 ret[seqlabel] = pathcopy
678
679                 basesIn := countBases(fasta.Bytes())
680                 log.Infof("%s %s fasta in %d coverage in %d path len %d low-quality %d", filelabel, seqlabel, fasta.Len(), basesIn, len(path), lowquality)
681                 stats = append(stats, importStats{
682                         InputFile:             filelabel,
683                         InputLabel:            seqlabel,
684                         InputLength:           fasta.Len(),
685                         InputCoverage:         basesIn,
686                         PathLength:            len(path),
687                         DroppedOutOfOrderTags: droppedOOO,
688                         DroppedRepeatedTags:   droppedDup,
689                 })
690
691                 totalPathLen += len(path)
692         }
693         log.Printf("%s tiled with total path len %d in %d sequences (skipped %d sequences that did not match chromosome regexp, skipped %d out-of-order tags)", filelabel, totalPathLen, len(ret), skippedSequences, totalFoundTags-totalPathLen)
694         return ret, stats, nil
695 }
696
697 func (tilelib *tileLibrary) Len() int64 {
698         return atomic.LoadInt64(&tilelib.variants)
699 }
700
701 // Return a tileLibRef for a tile with the given tag and sequence,
702 // adding the sequence to the library if needed.
703 func (tilelib *tileLibrary) getRef(tag tagID, seq []byte, usedByRef bool) tileLibRef {
704         dropSeq := false
705         if !tilelib.retainNoCalls {
706                 for _, b := range seq {
707                         if b != 'a' && b != 'c' && b != 'g' && b != 't' {
708                                 dropSeq = true
709                                 break
710                         }
711                 }
712         }
713         seqhash := blake2b.Sum256(seq)
714         var vlock sync.Locker
715
716         tilelib.mtx.RLock()
717         if len(tilelib.vlock) > int(tag) {
718                 vlock = tilelib.vlock[tag]
719         }
720         tilelib.mtx.RUnlock()
721
722         if vlock != nil {
723                 vlock.Lock()
724                 for i, varhash := range tilelib.variant[tag] {
725                         if varhash == seqhash {
726                                 vlock.Unlock()
727                                 return tileLibRef{Tag: tag, Variant: tileVariantID(i + 1)}
728                         }
729                 }
730                 vlock.Unlock()
731         } else {
732                 tilelib.mtx.Lock()
733                 if tilelib.variant == nil && tilelib.taglib != nil {
734                         tilelib.variant = make([][][blake2b.Size256]byte, tilelib.taglib.Len())
735                         tilelib.vlock = make([]sync.Locker, tilelib.taglib.Len())
736                         for i := range tilelib.vlock {
737                                 tilelib.vlock[i] = new(sync.Mutex)
738                         }
739                 }
740                 if int(tag) >= len(tilelib.variant) {
741                         oldlen := len(tilelib.vlock)
742                         for i := 0; i < oldlen; i++ {
743                                 tilelib.vlock[i].Lock()
744                         }
745                         // If we haven't seen the tag library yet (as
746                         // in a merge), tilelib.taglib.Len() is
747                         // zero. We can still behave correctly, we
748                         // just need to expand the tilelib.variant and
749                         // tilelib.vlock slices as needed.
750                         if int(tag) >= cap(tilelib.variant) {
751                                 // Allocate 2x capacity.
752                                 newslice := make([][][blake2b.Size256]byte, int(tag)+1, (int(tag)+1)*2)
753                                 copy(newslice, tilelib.variant)
754                                 tilelib.variant = newslice[:int(tag)+1]
755                                 newvlock := make([]sync.Locker, int(tag)+1, (int(tag)+1)*2)
756                                 copy(newvlock, tilelib.vlock)
757                                 tilelib.vlock = newvlock[:int(tag)+1]
758                         } else {
759                                 // Use previously allocated capacity,
760                                 // avoiding copy.
761                                 tilelib.variant = tilelib.variant[:int(tag)+1]
762                                 tilelib.vlock = tilelib.vlock[:int(tag)+1]
763                         }
764                         for i := oldlen; i < len(tilelib.vlock); i++ {
765                                 tilelib.vlock[i] = new(sync.Mutex)
766                         }
767                         for i := 0; i < oldlen; i++ {
768                                 tilelib.vlock[i].Unlock()
769                         }
770                 }
771                 vlock = tilelib.vlock[tag]
772                 tilelib.mtx.Unlock()
773         }
774
775         vlock.Lock()
776         for i, varhash := range tilelib.variant[tag] {
777                 if varhash == seqhash {
778                         vlock.Unlock()
779                         return tileLibRef{Tag: tag, Variant: tileVariantID(i + 1)}
780                 }
781         }
782         atomic.AddInt64(&tilelib.variants, 1)
783         tilelib.variant[tag] = append(tilelib.variant[tag], seqhash)
784         variant := tileVariantID(len(tilelib.variant[tag]))
785         vlock.Unlock()
786
787         if tilelib.retainTileSequences && !dropSeq {
788                 seqCopy := append([]byte(nil), seq...)
789                 if tilelib.seq2 == nil {
790                         tilelib.mtx.Lock()
791                         if tilelib.seq2 == nil {
792                                 tilelib.seq2lock = map[[2]byte]sync.Locker{}
793                                 m := map[[2]byte]map[[blake2b.Size256]byte][]byte{}
794                                 var k [2]byte
795                                 for i := 0; i < 256; i++ {
796                                         k[0] = byte(i)
797                                         for j := 0; j < 256; j++ {
798                                                 k[1] = byte(j)
799                                                 m[k] = map[[blake2b.Size256]byte][]byte{}
800                                                 tilelib.seq2lock[k] = &sync.Mutex{}
801                                         }
802                                 }
803                                 tilelib.seq2 = m
804                         }
805                         tilelib.mtx.Unlock()
806                 }
807                 var k [2]byte
808                 copy(k[:], seqhash[:])
809                 locker := tilelib.seq2lock[k]
810                 locker.Lock()
811                 tilelib.seq2[k][seqhash] = seqCopy
812                 locker.Unlock()
813         }
814
815         saveSeq := seq
816         if dropSeq {
817                 // Save the hash, but not the sequence
818                 saveSeq = nil
819         }
820         if tilelib.encoder != nil {
821                 tilelib.encoder.Encode(LibraryEntry{
822                         TileVariants: []TileVariant{{
823                                 Tag:      tag,
824                                 Ref:      usedByRef,
825                                 Variant:  variant,
826                                 Blake2b:  seqhash,
827                                 Sequence: saveSeq,
828                         }},
829                 })
830         }
831         if tilelib.onAddTileVariant != nil {
832                 tilelib.onAddTileVariant(tileLibRef{tag, variant}, seqhash, saveSeq)
833         }
834         return tileLibRef{Tag: tag, Variant: variant}
835 }
836
837 func (tilelib *tileLibrary) hashSequence(hash [blake2b.Size256]byte) []byte {
838         var partition [2]byte
839         copy(partition[:], hash[:])
840         return tilelib.seq2[partition][hash]
841 }
842
843 func (tilelib *tileLibrary) TileVariantSequence(libref tileLibRef) []byte {
844         if libref.Variant == 0 || len(tilelib.variant) <= int(libref.Tag) || len(tilelib.variant[libref.Tag]) < int(libref.Variant) {
845                 return nil
846         }
847         return tilelib.hashSequence(tilelib.variant[libref.Tag][libref.Variant-1])
848 }
849
850 // Tidy deletes unreferenced tile variants and renumbers variants so
851 // more common variants have smaller IDs.
852 func (tilelib *tileLibrary) Tidy() {
853         log.Print("Tidy: compute inref")
854         inref := map[tileLibRef]bool{}
855         for _, refseq := range tilelib.refseqs {
856                 for _, librefs := range refseq {
857                         for _, libref := range librefs {
858                                 inref[libref] = true
859                         }
860                 }
861         }
862         log.Print("Tidy: compute remap")
863         remap := make([][]tileVariantID, len(tilelib.variant))
864         throttle := throttle{Max: runtime.NumCPU() + 1}
865         for tag, oldvariants := range tilelib.variant {
866                 tag, oldvariants := tagID(tag), oldvariants
867                 if tag%1000000 == 0 {
868                         log.Printf("Tidy: tag %d", tag)
869                 }
870                 throttle.Acquire()
871                 go func() {
872                         defer throttle.Release()
873                         uses := make([]int, len(oldvariants))
874                         for _, cg := range tilelib.compactGenomes {
875                                 for phase := 0; phase < 2; phase++ {
876                                         cgi := int(tag)*2 + phase
877                                         if cgi < len(cg) && cg[cgi] > 0 {
878                                                 uses[cg[cgi]-1]++
879                                         }
880                                 }
881                         }
882
883                         // Compute desired order of variants:
884                         // neworder[x] == index in oldvariants that
885                         // should move to position x.
886                         neworder := make([]int, len(oldvariants))
887                         for i := range neworder {
888                                 neworder[i] = i
889                         }
890                         sort.Slice(neworder, func(i, j int) bool {
891                                 if cmp := uses[neworder[i]] - uses[neworder[j]]; cmp != 0 {
892                                         return cmp > 0
893                                 } else {
894                                         return bytes.Compare(oldvariants[neworder[i]][:], oldvariants[neworder[j]][:]) < 0
895                                 }
896                         })
897
898                         // Replace tilelib.variant[tag] with a new
899                         // re-ordered slice of hashes, and make a
900                         // mapping from old to new variant IDs.
901                         remaptag := make([]tileVariantID, len(oldvariants)+1)
902                         newvariants := make([][blake2b.Size256]byte, 0, len(neworder))
903                         for _, oldi := range neworder {
904                                 if uses[oldi] > 0 || inref[tileLibRef{Tag: tag, Variant: tileVariantID(oldi + 1)}] {
905                                         newvariants = append(newvariants, oldvariants[oldi])
906                                         remaptag[oldi+1] = tileVariantID(len(newvariants))
907                                 }
908                         }
909                         tilelib.variant[tag] = newvariants
910                         remap[tag] = remaptag
911                 }()
912         }
913         throttle.Wait()
914
915         // Apply remap to genomes and reference sequences, so they
916         // refer to the same tile variants using the changed IDs.
917         log.Print("Tidy: apply remap")
918         var wg sync.WaitGroup
919         for _, cg := range tilelib.compactGenomes {
920                 cg := cg
921                 wg.Add(1)
922                 go func() {
923                         defer wg.Done()
924                         for idx, variant := range cg {
925                                 cg[idx] = remap[tagID(idx/2)][variant]
926                         }
927                 }()
928         }
929         for _, refcs := range tilelib.refseqs {
930                 for _, refseq := range refcs {
931                         refseq := refseq
932                         wg.Add(1)
933                         go func() {
934                                 defer wg.Done()
935                                 for i, tv := range refseq {
936                                         refseq[i].Variant = remap[tv.Tag][tv.Variant]
937                                 }
938                         }()
939                 }
940         }
941         wg.Wait()
942         log.Print("Tidy: done")
943 }
944
945 func countBases(seq []byte) int {
946         n := 0
947         for _, c := range seq {
948                 if isbase[c] {
949                         n++
950                 }
951         }
952         return n
953 }