syncoder 0.0.0__tar.gz → 0.0.1a1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,17 @@
1
+ #some temp and build directories
2
+ __pycache__
3
+ dist/*
4
+ Docs/
5
+ data/
6
+ out/
7
+ #ingnore data files that may be used or generated during dev.
8
+ *.ipynb
9
+ *.dna
10
+ *.pdf
11
+ *.txt
12
+ *.json
13
+ *.zip
14
+ *.jpg
15
+ *.bin
16
+ #any actual py files are in src. anything in root is supposed to just for development.
17
+ /*.py
@@ -0,0 +1,39 @@
1
+ Metadata-Version: 2.4
2
+ Name: syncoder
3
+ Version: 0.0.1a1
4
+ Summary: A codec for DNA data storage
5
+ Project-URL: Homepage, https://example.com/TBD
6
+ Project-URL: Issues, https://example.com/TBD/issues
7
+ Author-email: Christopher Takahashi <cnt@uw.edu>
8
+ License-Expression: BSD-3-Clause
9
+ License-File: LICENSE
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+
13
+ # SyNCODER
14
+
15
+ ## Project Overview
16
+ A DNA codec. (Fill in.)
17
+
18
+ ## Features
19
+ Fill in here.
20
+
21
+ ## Installation
22
+ `codec.py` Can be included in a project and used as a local module or installed and used from `pip` :
23
+ ```
24
+ pip install --pre syncoder
25
+ ```
26
+ Note the requirement for the `--pre` flag as there is no official post release yet.
27
+
28
+ ## Usage
29
+ To be completed. For now see `tests/test_codec_pytest.py` for examples
30
+
31
+ ## Testing
32
+ It is sufficient to run
33
+ ```
34
+ pytest
35
+ ```
36
+ from the root directory. Currently coverage is only for features and parameters in active use. More tests to come.
37
+
38
+ ## License
39
+ BSD-3 clause. see `LICENSE` file in this directory.
@@ -0,0 +1,27 @@
1
+ # SyNCODER
2
+
3
+ ## Project Overview
4
+ A DNA codec. (Fill in.)
5
+
6
+ ## Features
7
+ Fill in here.
8
+
9
+ ## Installation
10
+ `codec.py` Can be included in a project and used as a local module or installed and used from `pip` :
11
+ ```
12
+ pip install --pre syncoder
13
+ ```
14
+ Note the requirement for the `--pre` flag as there is no official post release yet.
15
+
16
+ ## Usage
17
+ To be completed. For now see `tests/test_codec_pytest.py` for examples
18
+
19
+ ## Testing
20
+ It is sufficient to run
21
+ ```
22
+ pytest
23
+ ```
24
+ from the root directory. Currently coverage is only for features and parameters in active use. More tests to come.
25
+
26
+ ## License
27
+ BSD-3 clause. see `LICENSE` file in this directory.
@@ -13,7 +13,8 @@ dependencies:
13
13
  - biopython
14
14
  - jupyter
15
15
  - ipykernel
16
- - build
17
16
  - pip:
18
17
  - galois==0.4.*
18
+ - build
19
+ - twine
19
20
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "syncoder"
3
- version = "0.0.0"
3
+ version = "0.0.1a1"
4
4
  description = "A codec for DNA data storage"
5
5
  authors = [
6
6
  { name="Christopher Takahashi", email="cnt@uw.edu" }
@@ -19,4 +19,9 @@ packages = ["syncoder"]
19
19
 
20
20
  [build-system]
21
21
  requires = ["hatchling >= 1.26"]
22
- build-backend = "hatchling.build"
22
+ build-backend = "hatchling.build"
23
+
24
+ [tool.pytest.ini_options]
25
+ testpaths = [
26
+ "tests"
27
+ ]
@@ -0,0 +1,4 @@
1
+ from .codec import *
2
+ from .codec import _default_b32_alphabet, _default_b32_alphabet_alt
3
+ default_b32_alphabet = _default_b32_alphabet
4
+ default_b32_alphabet_alt = _default_b32_alphabet_alt
@@ -2,7 +2,8 @@ import os
2
2
  import struct
3
3
  import itertools
4
4
  import logging
5
- from typing import Optional,Union, Callable #requires python 3.5 or later
5
+ import typing
6
+ from typing import Optional,Union, Callable, Tuple #requires python 3.5 or later
6
7
 
7
8
 
8
9
  import numpy as np
@@ -18,7 +19,7 @@ import reedsolo
18
19
 
19
20
 
20
21
  lipsum = b"Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra nulla ut metus varius laoreet. Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet adipiscing sem neque sed ipsum." #cspell:disable-line
21
-
22
+
22
23
 
23
24
  def _int_to_baseN(n:int,base:int,length:int=-1)->list[int]:
24
25
  """Converts an integer to a base N number in little endian order.
@@ -255,7 +256,7 @@ class BaseNBlockCodec:
255
256
 
256
257
  return ic_chunks
257
258
 
258
- def decode(self, data: list[list[int]], index_start: int = 0) -> bytes:
259
+ def decode(self, data: list[list[int]], index_start: int = 0) -> Tuple[bytes,ArrayLike,ArrayLike,ArrayLike]:
259
260
  """
260
261
  Decodes the given data.
261
262
 
@@ -274,18 +275,20 @@ class BaseNBlockCodec:
274
275
  """
275
276
  # inner decode
276
277
  ## TODO: handle erasures in inner code.
277
- chunked_data = [self.inner_coder.decode(d,errors=True) for d in data]
278
+
279
+ # I'm sure it'll be a (FieldArray,int) ----------------V
280
+ chunked_data:list[Tuple[galois.FieldArray,int]] = [self.inner_coder.decode(d,errors=True) for d in data] #type:ignore
278
281
 
279
282
 
280
283
  # handle outer decode
281
- ordered_chunks = [None] * self.n_strands
282
- chunk_errors = [-1] * self.n_strands
284
+ ordered_chunks:list[bytes|None] = [None] * self.n_strands
285
+ chunk_errors:list[int] = [-1] * self.n_strands
283
286
  for chunk in chunked_data:
284
287
  chunk_int = _baseN_to_int(chunk[0].tolist(), self.inner_coder.field.order)
285
288
  chunk_index = chunk_int >> (self.data_chunk_size * 8) - index_start
286
289
  _mask = (2 ** (self.data_chunk_size * 8)) - 1
287
290
  chunk_int = chunk_int & _mask
288
- chunk_bytes = chunk_int.to_bytes(self.data_chunk_size, 'little')
291
+ chunk_bytes:bytes = chunk_int.to_bytes(self.data_chunk_size, 'little')
289
292
  #print(chunk_index)
290
293
  try:
291
294
  ordered_chunks[chunk_index] = chunk_bytes
@@ -299,6 +302,8 @@ class BaseNBlockCodec:
299
302
  if c is None:
300
303
  erasures.append(i) # remember where erasure is
301
304
  ordered_chunks[i] = b'\x00' * self.data_chunk_size # fill in erasures with zeros
305
+ #we're all bytes now, but the checker can't tell that.
306
+ ordered_chunks_bytes:list[bytes] = typing.cast(list[bytes],ordered_chunks)
302
307
 
303
308
  if self.outer_coder.c_exp==8:
304
309
  outer_dtype = np.uint8
@@ -306,7 +311,7 @@ class BaseNBlockCodec:
306
311
  outer_dtype = np.uint16
307
312
  else:
308
313
  raise ValueError("Unsupported outer code byte size.")
309
- data_np_encoded = np.array([np.frombuffer(c, dtype=outer_dtype) for c in ordered_chunks]).transpose()
314
+ data_np_encoded = np.array([np.frombuffer(c, dtype=outer_dtype) for c in ordered_chunks_bytes]).transpose()
310
315
  data_decoded_results = [self.outer_coder.decode(dc, erase_pos=erasures) for dc in data_np_encoded]
311
316
  data_decoded_chunks = np.array([d[0] for d in data_decoded_results],dtype=outer_dtype).transpose()
312
317
  data_decoded = data_decoded_chunks.flatten().tobytes()
@@ -336,7 +341,7 @@ class BaseNBlockCodec:
336
341
  _default_b32_alphabet = ['GCT', 'ACT', 'AGT', 'CTC', 'TGC', 'GAG', 'GCA', 'ATG', 'AGA', 'AGC', 'CAC', 'CAT', 'CAG', 'CTG', 'TCT', 'GAC', 'GTC', 'GTG', 'ACA', 'ACG', 'ATC', 'CTA', 'CGA', 'CGT', 'TAC', 'TAG', 'TCA', 'TCG', 'TGA', 'TGT', 'GAT', 'GTA']
337
342
  _default_b32_alphabet_alt = ['ATA', 'AAC', 'CCT', 'TAT', 'CCG', 'TGG', 'CGC', 'CGG', 'GTT', 'GGT', 'GCG', 'TCC', 'CCA', 'GGA', 'ACC', 'AGG', 'TAA', 'GCC', 'AAA', 'CCC', 'GGG', 'GGC', 'TTA', 'TTT', 'GAA', 'AAG', 'CTT', 'TTG', 'TTC', 'AAT', 'CAA', 'ATT']
338
343
 
339
- def find_avoid(seq:str, avoids:list)->tuple[int]:
344
+ def find_avoid(seq:str, avoids:list)->tuple[int,int]:
340
345
  """find index and length of avoided sequence """
341
346
  for a in avoids:
342
347
  loc = seq.find(a)
@@ -374,39 +379,53 @@ def longest_binder(a:str,b:str):
374
379
 
375
380
 
376
381
 
377
- def b32_to_DNA_optimize(file_data:list[list[int]],words:list[str], alternate_words:list[str], mask:Union[list[int],None]=None, penalty_fn:Union[Callable[[str],int],None]=None)->list[str]:
382
+ def b32_to_DNA_optimize(file_data:list[ArrayLike],words:list[str], alternate_words:list[str], mask:Union[ArrayLike,None]=None, nmasked=-1, penalty_fn:Union[Callable[[str],int],None]=None)->list[Tuple[bytes,int]]:
378
383
  """
379
384
  TODO: rename to bN_to_DNA_optimize, and add test.
385
+ file_data: 2d list of integral types (symbols)
380
386
  mask: list of ints. 0 for word, 1 for alternate word, -1 for don't care. None for no mask.
387
+ nmasked: number of masked words. -1 for all.
381
388
  penalty_fn: function that takes a DNA sequence and returns a score. penalty_fn(DNA_seq:str)->int
382
389
  """
383
- return [b32_to_DNA_optimize_single(x,words,alternate_words,penalty_fn=penalty_fn) for x in file_data]
390
+ if nmasked<0: #mask all
391
+ return [b32_to_DNA_optimize_single(x,words,alternate_words,mask,penalty_fn=penalty_fn) for x in file_data]
392
+ else:
393
+ masked_part = [b32_to_DNA_optimize_single(x,words,alternate_words,mask,penalty_fn=penalty_fn) for x in file_data[0:nmasked]]
394
+ unmasked_part = [b32_to_DNA_optimize_single(x,words,alternate_words,mask=None,penalty_fn=penalty_fn) for x in file_data[nmasked:]]
395
+ return masked_part + unmasked_part
384
396
 
385
397
 
386
- def b32_to_DNA_optimize_single(strand_data:list[int],words:list[str], alternate_words:list[str], mask:Union[list[int],None]=None ,penalty_fn=None)->str:
398
+ def b32_to_DNA_optimize_single(strand_data:ArrayLike, words:list[str], alternate_words:list[str], mask:Union[ArrayLike,None]=None ,penalty_fn=None)->Tuple[bytes,int]:
387
399
  """
388
400
  TODO: rename to bN_to_DNA_optimize_single
389
401
  TODO: add mask support.
402
+ strand_data: 1d list of integral types
390
403
  mask: list of ints. 0 for word, 1 for alternate word, -1 for don't care. None for no mask.
391
404
  """
405
+ strand_data = np.array(strand_data)
406
+
392
407
  if mask is not None:
393
- mutable_ind = np.where(np.array(mask)==-1)
408
+ mask = np.array(mask)
409
+ _m = np.full(len(strand_data),-1,dtype=np.int8)
410
+ _m[:len(mask)] = np.array(mask.copy()) #Probably don't need this copy.
411
+ mask = _m
412
+ mutable_ind = np.where(mask==-1)
413
+ mask[mutable_ind] = 0
394
414
  #raise NotImplementedError("mask not implemented yet")
395
415
  else:
416
+ mask = np.zeros(len(strand_data),dtype=np.uint8)
396
417
  mutable_ind = np.arange(len(strand_data))
397
- mask = mask.copy()
398
- mask[mutable_ind] = 0
399
-
400
- strand_data = np.array(strand_data)
401
- words = np.array(list(zip(words,alternate_words)),dtype="S")
418
+
419
+ words_lookup = np.array(list(zip(words,alternate_words)),dtype="S")
402
420
  picks = np.zeros(len(strand_data),dtype=np.uint8)
403
421
  picks[0:len(mask)] = mask
404
- dna_seq = b"".join(words[strand_data,picks])
422
+ dna_seq = b"".join(words_lookup[strand_data,picks])
405
423
  if penalty_fn is None:
406
424
  return dna_seq,0
407
425
  score = penalty_fn(dna_seq)
408
426
 
409
427
  #greedy search, stop when you can't find a better solution one step away.
428
+ #this may end on a local minimum.
410
429
  icount = 0
411
430
  while True:
412
431
  iteration_best_picks = picks.copy()
@@ -415,7 +434,7 @@ def b32_to_DNA_optimize_single(strand_data:list[int],words:list[str], alternate_
415
434
  for i in mutable_ind:
416
435
  new_picks = picks.copy()
417
436
  new_picks[i] = not new_picks[i]
418
- new_dna_seq = b"".join(words[strand_data,new_picks])
437
+ new_dna_seq = b"".join(words_lookup[strand_data,new_picks])
419
438
  new_score = penalty_fn(new_dna_seq)
420
439
  if new_score < iteration_best_score:
421
440
  iteration_best_picks = new_picks
@@ -457,11 +476,11 @@ def b32_to_DNA(file_data:list[list[int]],words:list[str], alternate_words:list[s
457
476
  dna.append(_s)
458
477
  return dna
459
478
 
460
- def dna_to_bN(dna: Union[list[str],list[bytes]] ,words:list[str], alternate_words:list[str])->list[int]:
461
- """ takes a list of strings and converts them to a list of base N ints"""
479
+ def dna_to_bN(dna: Union[list[str],list[bytes]] ,words:list[str], alternate_words:list[str])->list[list[int]]:
480
+ """ takes a list of strings and converts them to a list of lists of base N ints"""
462
481
 
463
- if isinstance(dna[0],bytes):
464
- dna = [x.decode() for x in dna]
482
+ if isinstance(dna[0],bytes): #assume the rest are bytes too.
483
+ dna = [x.decode() for x in typing.cast(list[bytes],dna)]
465
484
 
466
485
  wordlen = len(words[0])
467
486
  #build the reverse lookup table
@@ -471,16 +490,16 @@ def dna_to_bN(dna: Union[list[str],list[bytes]] ,words:list[str], alternate_word
471
490
  bNlut[alternate_words[ind]] = ind
472
491
 
473
492
 
474
- bNdatalist = []
493
+ bNdatalist:list[list[int]] = []
475
494
  for d in dna:
476
- bNdata = []
495
+ bNdata:list[int] = []
477
496
  for ind in range(0,len(d),wordlen):
478
497
  w = d[ind:ind+wordlen]
479
498
  bNdata.append(bNlut[w])
480
499
  bNdatalist.append(bNdata)
481
500
  return bNdatalist
482
501
 
483
- def dna_to_b32(dna: Union[list[str],list[bytes]] ,words:list[str], alternate_words:list[str])->list[int]:
502
+ def dna_to_b32(dna: Union[list[str],list[bytes]] ,words:list[str], alternate_words:list[str])->list[list[int]]:
484
503
  assert len(words) == 32
485
504
  return dna_to_bN(dna,words,alternate_words)
486
505
 
@@ -602,15 +621,21 @@ def remove_bytes(data:bytes, insert:int|bytes, chunk_len:int, num_inserts:int) -
602
621
  Returns:
603
622
  bytes: The modified byte array with the inserted bytes removed.
604
623
  """
624
+ have_insert_seq = False
605
625
  if isinstance(insert,bytes):
606
626
  insert_len = len(insert)
627
+ have_insert_seq = True
628
+ elif isinstance(insert,int):
629
+ if insert <= 0:
630
+ raise ValueError("insert must be a positive integer")
631
+ insert_len:int = insert
607
632
  else:
608
- insert = None
633
+ raise ValueError("insert must be a bytes or int")
609
634
 
610
635
  data = bytearray(data)
611
636
  for i in reversed(range(num_inserts)):
612
637
  ins_index = i * chunk_len
613
- if insert is not None:
638
+ if have_insert_seq:
614
639
  if data[ins_index:ins_index + insert_len] != insert:
615
640
  raise ValueError("insert bytes do not match bytes to be removed")
616
641
  del data[ins_index:ins_index + insert_len]
@@ -4,7 +4,7 @@ import random
4
4
  import numpy as np
5
5
  from itertools import product
6
6
 
7
- import codec
7
+ import syncoder as codec
8
8
 
9
9
  #logging.basicConfig(level=logging.DEBUG,stream=sys.stdout)
10
10
  #numba_logger = logging.getLogger('numba')
@@ -27,7 +27,7 @@ def test_encode_decode():
27
27
  def test_coded_to_bases():
28
28
  #TODO: test optimize too
29
29
 
30
- test_encoded_data = np.random.randint(0,32,(10,31))
30
+ test_encoded_data = np.random.randint(0,32,(10,31),dtype=np.uint8)
31
31
  DNA_with_scores = codec.b32_to_DNA_optimize(test_encoded_data,codec._default_b32_alphabet,codec._default_b32_alphabet_alt)
32
32
  DNA = [x[0] for x in DNA_with_scores]
33
33
  test_decoded_data = codec.dna_to_b32(DNA,codec._default_b32_alphabet,codec._default_b32_alphabet_alt)
@@ -54,6 +54,8 @@ def test_dna_to_bN():
54
54
  assert np.array_equal(data, decode_data)
55
55
 
56
56
  def test_dna_to_bytes():
57
+ _int_to_baseN = codec.codec._int_to_baseN
58
+
57
59
  #TODO: adapt to workwith not b32 too.
58
60
  #answer = [64, 79, 231, 126, 228, 124, 3]
59
61
  alphabet =codec._default_b32_alphabet
@@ -70,7 +72,7 @@ def test_dna_to_bytes():
70
72
 
71
73
  dna_bytes, mask = codec.dna_to_bytes(DNA,alphabet,alphabet_alt)
72
74
 
73
- dna_baseN = codec._int_to_baseN(int.from_bytes(dna_bytes,"little"),len(alphabet))
75
+ dna_baseN = _int_to_baseN(int.from_bytes(dna_bytes,"little"),len(alphabet))
74
76
  assert dna_baseN == answer32
75
77
 
76
78
  _t = np.array(dna_baseN)
@@ -78,3 +80,28 @@ def test_dna_to_bytes():
78
80
  generated_dna = "".join(full_alphabet[dna_baseN_with_alt])
79
81
 
80
82
  assert generated_dna == DNA
83
+
84
+ def test_inserts():
85
+ coder = codec.BaseNBlockCodec(inner_alphabet_size=32,inner_d=5,inner_n=29,n_strands=2000,n_redundant_strands=100)
86
+ n_inserts = 200
87
+ insert = "GCTCTTCCCACCACCATTGCCTTCTTCTTG"
88
+ insert_bytes, imask = codec.dna_to_bytes(insert,codec._default_b32_alphabet,codec._default_b32_alphabet_alt)
89
+ total_ibytes = len(insert_bytes)*n_inserts
90
+ data_size = coder.block_capacity_bytes - total_ibytes
91
+
92
+
93
+ test_data = np.random.randint(0,255,data_size,dtype=np.uint8).tobytes()
94
+
95
+ inserted_data = codec.insert_bytes(test_data,insert_bytes,coder.data_chunk_size,n_inserts)
96
+ assert len(inserted_data) == coder.block_capacity_bytes
97
+
98
+ enc_d = coder.encode(inserted_data)
99
+ data_decodec,__oe,__oer,__ie = coder.decode(enc_d)
100
+ assert data_decodec == inserted_data
101
+
102
+ test_dna_out = codec.b32_to_DNA_optimize(enc_d,codec._default_b32_alphabet,codec._default_b32_alphabet_alt,mask=imask,nmasked=n_inserts)
103
+ test_dna = [x[0] for x in test_dna_out]
104
+ for strand in test_dna[:n_inserts]:
105
+ assert strand.decode().startswith(insert)
106
+
107
+ assert codec.dna_to_bN(test_dna,codec._default_b32_alphabet,codec._default_b32_alphabet_alt) == enc_d
syncoder-0.0.0/.gitignore DELETED
@@ -1,11 +0,0 @@
1
- __pycache__/*
2
- Docs/
3
- *.ipynb
4
- *.dna
5
- *.pdf
6
- *.txt
7
- *.json
8
- *.zip
9
- *.jpg
10
- *.bin
11
- debugging.py
syncoder-0.0.0/PKG-INFO DELETED
@@ -1,13 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: syncoder
3
- Version: 0.0.0
4
- Summary: A codec for DNA data storage
5
- Project-URL: Homepage, https://example.com/TBD
6
- Project-URL: Issues, https://example.com/TBD/issues
7
- Author-email: Christopher Takahashi <cnt@uw.edu>
8
- License-Expression: BSD-3-Clause
9
- License-File: LICENSE
10
- Requires-Python: >=3.9
11
- Description-Content-Type: text/markdown
12
-
13
- Readme coming soon.
syncoder-0.0.0/README.md DELETED
@@ -1 +0,0 @@
1
- Readme coming soon.
syncoder-0.0.0/foo.py DELETED
@@ -1,4 +0,0 @@
1
-
2
- from src import *
3
-
4
- print(nextfn(5))
@@ -1 +0,0 @@
1
- __all__ = ["bar.nextfn"]
File without changes
File without changes