syncoder 0.0.2a1__tar.gz → 0.0.4a1__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.
@@ -14,5 +14,6 @@ out/
14
14
  *.jpg
15
15
  *.bin
16
16
  .pypirc
17
+ *.bat
17
18
  #any actual py files are in src. anything in root is supposed to just for development.
18
19
  /*.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: syncoder
3
- Version: 0.0.2a1
3
+ Version: 0.0.4a1
4
4
  Summary: A codec for DNA data storage
5
5
  Project-URL: Homepage, https://example.com/TBD
6
6
  Project-URL: Issues, https://example.com/TBD/issues
@@ -3,4 +3,4 @@ from .codec import _default_b32_alphabet, _default_b32_alphabet_alt
3
3
  default_b32_alphabet = _default_b32_alphabet
4
4
  default_b32_alphabet_alt = _default_b32_alphabet_alt
5
5
 
6
- __version__ = "0.0.2a1"
6
+ __version__ = "0.0.4a1"
@@ -3,7 +3,7 @@ import struct
3
3
  import itertools
4
4
  import logging
5
5
  import typing
6
- from typing import Optional,Union, Callable, Tuple #requires python 3.5 or later
6
+ from typing import Optional,Union, Callable, Tuple, Literal, cast #requires python 3.8 or later
7
7
 
8
8
 
9
9
  import numpy as np
@@ -62,15 +62,39 @@ def _baseN_to_int(digits: list[int], base: int) -> int:
62
62
  n = n*base + d
63
63
  return n
64
64
 
65
- def computeNStrands(nbytes:int,
65
+ def compute_num_strands(nbytes:int,
66
66
  inner_alphabet_size:int=32,
67
67
  inner_d:int=5,
68
68
  inner_n:Optional[int]=None,
69
69
  n_redundant_strands:int=5)->int:
70
-
70
+ """
71
+ Computes the number of strands needed to encode a given number of bytes.
72
+
73
+ Args:
74
+ nbytes (int): The number of bytes to encode.
75
+ inner_alphabet_size (int, optional): Size of the inner alphabet (must be a prime power). Defaults to 32.
76
+ inner_d (int, optional): Inner codeword distance. Defaults to 5.
77
+ inner_n (Optional[int], optional): Number of symbols in inner block (n < inner_alphabet_size). Defaults to inner_alphabet_size-1.
78
+ n_redundant_strands (int, optional): Number of redundant strands. Defaults to 5.
79
+
80
+ Returns:
81
+ int: The minimum number of strands required to encode the given number of bytes.
82
+ """
71
83
  #TODO: do some algebra to solve for n_strands.
72
- min_m=1
73
- max_m=2**16-1
84
+ if inner_n is None:
85
+ inner_n = inner_alphabet_size - 1
86
+
87
+ k = inner_n - (inner_d - 1)
88
+ bits_per_strand_max = np.ceil(np.log2(inner_alphabet_size))*k
89
+ bits_per_strand_min = np.floor(np.log2(inner_alphabet_size))*k-16 #16 bits for largest possible index.
90
+ #min_m=1
91
+ min_m = int( np.floor(nbytes*8 / bits_per_strand_max + n_redundant_strands) )
92
+ #max_m=2**16-1
93
+ max_m = int( np.ceil(nbytes*8 / bits_per_strand_min + n_redundant_strands) )
94
+
95
+ _logger = logging.getLogger()
96
+ _t = _logger.getEffectiveLevel()
97
+ _logger.setLevel(logging.ERROR) #suppress debug messages
74
98
  while min_m<max_m:
75
99
  mid = (min_m+max_m)//2
76
100
  codec = BaseNBlockCodec(inner_alphabet_size=inner_alphabet_size,
@@ -82,14 +106,8 @@ def computeNStrands(nbytes:int,
82
106
  min_m = mid+1
83
107
  else:
84
108
  max_m = mid
109
+ _logger.setLevel(_t) #restore logger level
85
110
  return min_m
86
- #index_bits = np.ceil(np.log2(n_strands)).astype(int)
87
- #k = inner_n - (inner_d - 1)
88
- #data_chunk_size = int(k*np.log2(inner_alphabet_size)-index_bits) // 8
89
- #block_capacity_bytes = data_chunk_size*(n_strands - n_redundant_strands)
90
-
91
-
92
-
93
111
 
94
112
  class BaseNBlockCodec:
95
113
  def __init__(self,
@@ -110,8 +128,6 @@ class BaseNBlockCodec:
110
128
  n_strands (int): Number of "strands" stored by this codec including redundant strands.
111
129
  max_strand_index (Optional[int]): Maximum guaranteed index. Default: n_strands.
112
130
  """
113
- #TODO: handle more than 255 total strands.
114
- #this was formally a parameter but probably better to keep it fixed or automatically calculated.
115
131
  if n_strands<=255:
116
132
  self.outer_alphabet_size_bytes = 1
117
133
  else:
@@ -119,6 +135,10 @@ class BaseNBlockCodec:
119
135
  if n_strands>(2**(8*self.outer_alphabet_size_bytes)-1):
120
136
  raise ValueError("Too many strands for outer code.")
121
137
  self.n_strands = n_strands
138
+ if max_strand_index is None:
139
+ max_strand_index = n_strands
140
+ if max_strand_index<n_strands:
141
+ raise ValueError("max_strand_index must be greater than or equal to n_strands.")
122
142
 
123
143
  self.n_message_strands = n_strands - n_redundant_strands
124
144
 
@@ -140,13 +160,13 @@ class BaseNBlockCodec:
140
160
  # sizes in bytes:
141
161
  # |<---- self.data_chunk_size ----->|<- . ->|
142
162
  # ^ = self.index_bits <-- in bits not bytes.
143
- # note: actual k remains alphabet_size-1, but the code is shorted (0 padded) to inner_n symbols.
163
+ # note: actual k remains alphabet_size-1, but the code is shortened (0 padded) to inner_n symbols.
144
164
  k = inner_n - (inner_d - 1)
145
165
  self.inner_k = k
146
166
  q = self.inner_coder.field.order
147
167
 
148
168
  #number of bytes needed to store the index
149
- self.index_bits = np.ceil(np.log2(self.n_strands)).astype(int)
169
+ self.index_bits = np.ceil(np.log2(max_strand_index)).astype(int)
150
170
  logging.debug("bits per strand used for indexing: {}".format(self.index_bits))
151
171
 
152
172
  #message length in bytes (without index)
@@ -286,7 +306,7 @@ class BaseNBlockCodec:
286
306
  chunk_errors:list[int] = [-1] * self.n_strands
287
307
  for chunk in chunked_data:
288
308
  chunk_int = _baseN_to_int(chunk[0].tolist(), self.inner_coder.field.order)
289
- chunk_index = chunk_int >> (self.data_chunk_size * 8) - index_start
309
+ chunk_index = (chunk_int >> (self.data_chunk_size * 8)) - index_start
290
310
  _mask = (2 ** (self.data_chunk_size * 8)) - 1
291
311
  chunk_int = chunk_int & _mask
292
312
  chunk_bytes:bytes = chunk_int.to_bytes(self.data_chunk_size, 'little')
@@ -336,6 +356,63 @@ class BaseNBlockCodec:
336
356
  logging.warning("Outer code errors found on strand {}, byte {}.".format(*e))
337
357
 
338
358
  return data_decoded, erasures, errors, chunk_errors
359
+
360
+ def extract_index_dna(self, dna: str|bytes, words:list[str], alternate_words:list[str], error_check: bool = False, padding: Literal["left","right"]="right") -> int:
361
+ """
362
+ Extracts the index from the given DNA strand.
363
+
364
+ Args:
365
+ dna (str|bytes): The input DNA strand from which to extract the index.
366
+ error_check (bool, optional): If True, the inner_codec will be used to
367
+ attempt to check for errors. Defaults to False.
368
+ padding Literal["left","right"], optional): Assume any missing bases are left or right of index. Defaults to "right".
369
+ Note padded sequences are unlikely to error correct properly.
370
+ words (list[str]): The primary set of base words used, in order, for encoding.
371
+ alternate_words (list[str]): The alternate (aka synonymous) set of words used, in order, for encoding.
372
+
373
+ Returns:
374
+ int: The index. or -1 if error_check==True and unrecoverable error found.
375
+ """
376
+ word_len:int = len(words[0])
377
+ if isinstance(dna,bytes):
378
+ dna= dna.decode()
379
+ dna_s:str = cast(str,dna)
380
+
381
+ if padding == "right":
382
+ _p = self.inner_k*word_len - len(dna_s)
383
+ if _p>0: #if i have less than a full codeword of bases
384
+ dna_s = dna_s + "A"*_p
385
+ elif padding == "left":
386
+ _p = self.inner_k*word_len - len(dna_s)
387
+ if _p>0: #if i have less than a full codeword of bases
388
+ dna_s = "A"*_p + dna_s
389
+
390
+ data = dna_to_bN([dna_s],words,alternate_words)[0]
391
+
392
+ return self.extract_index(data,error_check)
393
+
394
+ def extract_index(self, data: list[int], error_check: bool = False) -> int:
395
+ """
396
+ Extracts the index from the given DNA strand.
397
+
398
+ Args:
399
+ data list[int]: The input data from which to extract the index.
400
+ If a list[int] is given, it is assumed be be inner code symbols.
401
+ error_check (bool, optional): If True, the inner_codec will be used to
402
+ attempt to check for errors. Defaults to False.
403
+
404
+ Returns:
405
+ int: The index. or -1 if error_check==True and unrecoverable error found.
406
+ """
407
+ if error_check == False:
408
+ data = data[0:self.inner_k] #extract message
409
+ else:
410
+ data, n_corrected = self.inner_coder.decode(data,errors=True)[0].tolist()
411
+ if n_corrected < 0:
412
+ return -1
413
+ chunk_int = _baseN_to_int(data, self.inner_coder.field.order)
414
+ chunk_index = (chunk_int >> (self.data_chunk_size * 8))
415
+ return chunk_index
339
416
 
340
417
 
341
418
  #convert base N into DNA.
@@ -4,7 +4,8 @@ import random
4
4
  import numpy as np
5
5
  from itertools import product
6
6
 
7
- import syncoder as codec
7
+ import syncoder
8
+ codec = syncoder
8
9
 
9
10
  #logging.basicConfig(level=logging.DEBUG,stream=sys.stdout)
10
11
  #numba_logger = logging.getLogger('numba')
@@ -91,7 +92,6 @@ def test_inserts():
91
92
  total_ibytes = len(insert_bytes)*n_inserts
92
93
  data_size = coder.block_capacity_bytes - total_ibytes
93
94
 
94
-
95
95
  test_data = np.random.randint(0,255,data_size,dtype=np.uint8).tobytes()
96
96
 
97
97
  inserted_data = codec.insert_bytes(test_data,insert_bytes,coder.data_chunk_size,n_inserts)
@@ -107,3 +107,34 @@ def test_inserts():
107
107
  assert strand.decode().startswith(insert)
108
108
 
109
109
  assert codec.dna_to_bN(test_dna,codec._default_b32_alphabet,codec._default_b32_alphabet_alt) == enc_d
110
+
111
+ def test_compute_n_strands():
112
+ nbytes = 26600
113
+ ns = syncoder.compute_num_strands(nbytes,32,5,29,100)
114
+ data = np.random.randint(0,255,nbytes,dtype=np.uint8).tobytes()
115
+ coder = codec.BaseNBlockCodec(inner_alphabet_size=32,inner_d=5,inner_n=29,n_strands=ns,n_redundant_strands=100)
116
+ enc_d = coder.encode(data)
117
+ assert len(enc_d) == ns
118
+
119
+ def test_nonzero_index():
120
+ coder = syncoder.BaseNBlockCodec(inner_alphabet_size=32,inner_d=5,inner_n=31,n_strands=10,n_redundant_strands=3,max_strand_index=3000)
121
+ data_size = coder.block_capacity_bytes
122
+
123
+ test_data = np.random.randint(0,255,data_size,dtype=np.uint8).tobytes()
124
+ enc_d = coder.encode(test_data,index_start=2500)
125
+ ded_d = coder.decode(enc_d,index_start=2500)
126
+ assert ded_d[0] == test_data
127
+
128
+ dna = codec.b32_to_DNA_optimize(enc_d,codec._default_b32_alphabet,codec._default_b32_alphabet_alt) #type: ignore
129
+
130
+ index = coder.extract_index_dna(dna[0][0],
131
+ words = syncoder._default_b32_alphabet,
132
+ alternate_words= syncoder._default_b32_alphabet_alt,
133
+ error_check=False)
134
+ assert index == 2500
135
+ offset = 5
136
+ index = coder.extract_index_dna(dna[offset][0],
137
+ words = syncoder._default_b32_alphabet,
138
+ alternate_words= syncoder._default_b32_alphabet_alt,
139
+ error_check=False)
140
+ assert index == 2500+offset
File without changes
File without changes
File without changes