syncoder 0.0.1a1__tar.gz → 0.0.3a1__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.
@@ -13,5 +13,7 @@ out/
13
13
  *.zip
14
14
  *.jpg
15
15
  *.bin
16
+ .pypirc
17
+ *.bat
16
18
  #any actual py files are in src. anything in root is supposed to just for development.
17
19
  /*.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: syncoder
3
- Version: 0.0.1a1
3
+ Version: 0.0.3a1
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
@@ -8,6 +8,9 @@ Author-email: Christopher Takahashi <cnt@uw.edu>
8
8
  License-Expression: BSD-3-Clause
9
9
  License-File: LICENSE
10
10
  Requires-Python: >=3.9
11
+ Requires-Dist: galois>=0.4
12
+ Requires-Dist: numpy>=1.20
13
+ Requires-Dist: reedsolo>=1.7
11
14
  Description-Content-Type: text/markdown
12
15
 
13
16
  # SyNCODER
@@ -2,7 +2,7 @@ name: codec-user
2
2
  channels:
3
3
  - conda-forge
4
4
  dependencies:
5
- - python=3.12
5
+ - python>=3.12
6
6
  - numpy=2.*
7
7
  - pytest
8
8
  - reedsolo=1.7.*
@@ -2,7 +2,7 @@ name: codec-dev
2
2
  channels:
3
3
  - conda-forge
4
4
  dependencies:
5
- - python=3.12
5
+ - python>=3.12
6
6
  - numpy=2.*
7
7
  - matplotlib=3.*
8
8
  - pytest
@@ -2,7 +2,7 @@ name: codec-test
2
2
  channels:
3
3
  - conda-forge
4
4
  dependencies:
5
- - python=3.12
5
+ - python>=3.12
6
6
  - numpy=2.*
7
7
  - pytest
8
8
  - reedsolo=1.7.*
@@ -1,6 +1,11 @@
1
1
  [project]
2
2
  name = "syncoder"
3
- version = "0.0.1a1"
3
+ dynamic = ["version"]
4
+ dependencies = [
5
+ "numpy>=1.20",
6
+ "galois>=0.4",
7
+ "reedsolo>=1.7"
8
+ ]
4
9
  description = "A codec for DNA data storage"
5
10
  authors = [
6
11
  { name="Christopher Takahashi", email="cnt@uw.edu" }
@@ -24,4 +29,7 @@ build-backend = "hatchling.build"
24
29
  [tool.pytest.ini_options]
25
30
  testpaths = [
26
31
  "tests"
27
- ]
32
+ ]
33
+
34
+ [tool.hatch.version]
35
+ path = "src/syncoder/__init__.py"
@@ -1,4 +1,6 @@
1
1
  from .codec import *
2
2
  from .codec import _default_b32_alphabet, _default_b32_alphabet_alt
3
3
  default_b32_alphabet = _default_b32_alphabet
4
- default_b32_alphabet_alt = _default_b32_alphabet_alt
4
+ default_b32_alphabet_alt = _default_b32_alphabet_alt
5
+
6
+ __version__ = "0.0.3a1"
@@ -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
 
@@ -142,10 +162,11 @@ class BaseNBlockCodec:
142
162
  # ^ = self.index_bits <-- in bits not bytes.
143
163
  # note: actual k remains alphabet_size-1, but the code is shorted (0 padded) to inner_n symbols.
144
164
  k = inner_n - (inner_d - 1)
165
+ self.inner_k = k
145
166
  q = self.inner_coder.field.order
146
167
 
147
168
  #number of bytes needed to store the index
148
- 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)
149
170
  logging.debug("bits per strand used for indexing: {}".format(self.index_bits))
150
171
 
151
172
  #message length in bytes (without index)
@@ -248,7 +269,7 @@ class BaseNBlockCodec:
248
269
  chunk += index<<(self.data_chunk_size*8)
249
270
  index += 1
250
271
  #but.. we still need to pad out the symbol stream.
251
- chunk = _int_to_baseN(chunk,self.inner_coder.field.order,length=self.inner_coder.k)
272
+ chunk = _int_to_baseN(chunk,self.inner_coder.field.order,length=self.inner_k)
252
273
  chunks.append(chunk)
253
274
 
254
275
  #apply inner code
@@ -285,7 +306,7 @@ class BaseNBlockCodec:
285
306
  chunk_errors:list[int] = [-1] * self.n_strands
286
307
  for chunk in chunked_data:
287
308
  chunk_int = _baseN_to_int(chunk[0].tolist(), self.inner_coder.field.order)
288
- chunk_index = chunk_int >> (self.data_chunk_size * 8) - index_start
309
+ chunk_index = (chunk_int >> (self.data_chunk_size * 8)) - index_start
289
310
  _mask = (2 ** (self.data_chunk_size * 8)) - 1
290
311
  chunk_int = chunk_int & _mask
291
312
  chunk_bytes:bytes = chunk_int.to_bytes(self.data_chunk_size, 'little')
@@ -386,7 +407,7 @@ def b32_to_DNA_optimize(file_data:list[ArrayLike],words:list[str], alternate_wor
386
407
  mask: list of ints. 0 for word, 1 for alternate word, -1 for don't care. None for no mask.
387
408
  nmasked: number of masked words. -1 for all.
388
409
  penalty_fn: function that takes a DNA sequence and returns a score. penalty_fn(DNA_seq:str)->int
389
- """
410
+ """
390
411
  if nmasked<0: #mask all
391
412
  return [b32_to_DNA_optimize_single(x,words,alternate_words,mask,penalty_fn=penalty_fn) for x in file_data]
392
413
  else:
@@ -398,7 +419,6 @@ def b32_to_DNA_optimize(file_data:list[ArrayLike],words:list[str], alternate_wor
398
419
  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]:
399
420
  """
400
421
  TODO: rename to bN_to_DNA_optimize_single
401
- TODO: add mask support.
402
422
  strand_data: 1d list of integral types
403
423
  mask: list of ints. 0 for word, 1 for alternate word, -1 for don't care. None for no mask.
404
424
  """
@@ -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')
@@ -23,12 +24,14 @@ def test_encode_decode():
23
24
 
24
25
  out_text = c.decode( coded )[0]
25
26
  assert in_text == out_text
27
+ #check inner_n is causing the correct number of bases
28
+ assert len(coded[0]) == 30
26
29
 
27
30
  def test_coded_to_bases():
28
31
  #TODO: test optimize too
29
32
 
30
33
  test_encoded_data = np.random.randint(0,32,(10,31),dtype=np.uint8)
31
- DNA_with_scores = codec.b32_to_DNA_optimize(test_encoded_data,codec._default_b32_alphabet,codec._default_b32_alphabet_alt)
34
+ DNA_with_scores = codec.b32_to_DNA_optimize(test_encoded_data,codec._default_b32_alphabet,codec._default_b32_alphabet_alt) # type: ignore
32
35
  DNA = [x[0] for x in DNA_with_scores]
33
36
  test_decoded_data = codec.dna_to_b32(DNA,codec._default_b32_alphabet,codec._default_b32_alphabet_alt)
34
37
  print(test_encoded_data.flatten())
@@ -89,7 +92,6 @@ def test_inserts():
89
92
  total_ibytes = len(insert_bytes)*n_inserts
90
93
  data_size = coder.block_capacity_bytes - total_ibytes
91
94
 
92
-
93
95
  test_data = np.random.randint(0,255,data_size,dtype=np.uint8).tobytes()
94
96
 
95
97
  inserted_data = codec.insert_bytes(test_data,insert_bytes,coder.data_chunk_size,n_inserts)
@@ -99,9 +101,26 @@ def test_inserts():
99
101
  data_decodec,__oe,__oer,__ie = coder.decode(enc_d)
100
102
  assert data_decodec == inserted_data
101
103
 
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)
104
+ test_dna_out = codec.b32_to_DNA_optimize(enc_d,codec._default_b32_alphabet,codec._default_b32_alphabet_alt,mask=imask,nmasked=n_inserts) # type: ignore
103
105
  test_dna = [x[0] for x in test_dna_out]
104
106
  for strand in test_dna[:n_inserts]:
105
107
  assert strand.decode().startswith(insert)
106
108
 
107
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
File without changes
File without changes