syncoder 0.0.4a2__tar.gz → 0.0.5a0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: syncoder
3
- Version: 0.0.4a2
3
+ Version: 0.0.5a0
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
@@ -0,0 +1,10 @@
1
+ name: codec-user
2
+ channels:
3
+ - conda-forge
4
+ dependencies:
5
+ - python>=3.13
6
+ - numpy=2.2.6
7
+ - numba=0.61.*
8
+ - pip:
9
+ - galois>=0.4.6
10
+
@@ -0,0 +1,21 @@
1
+ name: codec-dev
2
+ channels:
3
+ - conda-forge
4
+ dependencies:
5
+ - python>=3.13
6
+ - pip
7
+ - "libblas=*=*mkl"
8
+ - numpy=2.2.6 # version pinned for galois 0.4.6
9
+ - matplotlib=3.*
10
+ - numba=0.61.* # version pinned for galois 0.4.6
11
+ - pytest
12
+ - sphinx=7.*
13
+ - sphinx_rtd_theme
14
+ - biopython
15
+ - jupyter
16
+ - ipykernel
17
+ - twine
18
+ - pip:
19
+ - galois>=0.4.6
20
+ - build
21
+
@@ -0,0 +1,15 @@
1
+ name: codec-test
2
+ channels:
3
+ - conda-forge
4
+ dependencies:
5
+ - python>=3.13
6
+ - pip
7
+ - numpy=2.2.6 # version pinned for galois 0.4.6
8
+ - numba=0.61.* # version pinned for galois 0.4.6
9
+ - pytest
10
+ - sphinx=7.*
11
+ - sphinx_rtd_theme
12
+ - twine
13
+ - pip:
14
+ - galois>=0.4.6
15
+ - build
@@ -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.4a2"
6
+ __version__ = "0.0.5a0"
@@ -124,7 +124,9 @@ class BaseNBlockCodec:
124
124
  inner_n:Optional[int]=None,
125
125
  n_redundant_strands:int=5,
126
126
  n_strands:int=100,
127
- max_strand_index:Optional[int]=None):
127
+ max_strand_index:Optional[int]=None,
128
+ index_type:Literal["binary", "inner symbol", "inner"]="binary",
129
+ index_location:Literal["middle", "beginning", "end"]="middle"):
128
130
  """
129
131
  Initialize the Codec object.
130
132
 
@@ -135,6 +137,16 @@ class BaseNBlockCodec:
135
137
  n_redundant_strands (int): Number of redundant strands.
136
138
  n_strands (int): Number of "strands" stored by this codec including redundant strands.
137
139
  max_strand_index (Optional[int]): Maximum guaranteed index. Default: n_strands.
140
+ index_type (str): Type of index to use. Options: "binary", "inner symbol", "inner". Default: "binary".
141
+ index_location (str): Location of the index within the inner code. Options: "middle", "beginning", "end". Default: "middle".
142
+
143
+ index_type "binary" integrates the index into the data stream as a binary number.
144
+ This may help packing efficiency.
145
+ index_type "inner symbol"/"inner" integrates the index inner code symbol symbols.
146
+ This may help packing efficiency and always simplifies index extraction.
147
+ index_location "middle" (default) places the index after the data symbols but before the redundancy symbols.
148
+ index_location "beginning" places the index before the data symbols.
149
+ index_location "end" places the index after the redundancy symbols. (not implemented)
138
150
  """
139
151
  if n_strands<=255:
140
152
  self.outer_alphabet_size_bytes = 1
@@ -147,6 +159,20 @@ class BaseNBlockCodec:
147
159
  max_strand_index = n_strands
148
160
  if max_strand_index<n_strands:
149
161
  raise ValueError("max_strand_index must be greater than or equal to n_strands.")
162
+
163
+ if index_type not in {"binary","inner symbol","inner"}:
164
+ raise ValueError("index_type must be 'binary' or 'inner symbol'.")
165
+ #"inner" and "inner symbol" are synomymous.
166
+ self.index_type = "inner" if index_type == "inner symbol" else index_type
167
+
168
+ if index_location in {"middle","beginning","end"}:
169
+ self.index_location = index_location
170
+ if index_location == "end":
171
+ raise NotImplementedError("index_location 'end' is not implemented.")
172
+ if index_location != "middle" and index_type == "binary":
173
+ raise ValueError("index_location 'beginning' or 'end' not supported for index_type 'binary'.")
174
+ else:
175
+ raise ValueError("index_location must be 'middle', 'beginning', or 'end'.")
150
176
 
151
177
  self.n_message_strands = n_strands - n_redundant_strands
152
178
 
@@ -178,25 +204,45 @@ class BaseNBlockCodec:
178
204
  logging.debug("bits per strand used for indexing: {}".format(self.index_bits))
179
205
 
180
206
  #message length in bytes (without index)
181
- self.data_chunk_size = int(k*np.log2(q)-self.index_bits) // 8
207
+ if self.index_type == "binary":
208
+ self.data_chunk_size = int(k*np.log2(q)-self.index_bits) // 8
209
+ elif self.index_type == "inner":
210
+ self.index_symbols = len(_int_to_baseN(max_strand_index,q))
211
+ self.data_symbols = k-self.index_symbols
212
+ self.data_chunk_size = int(self.data_symbols*np.log2(q)) // 8
213
+ assert k == self.data_symbols + self.index_symbols
214
+ logging.debug("data chunk size in symbols: {}".format(self.data_symbols))
215
+ logging.debug(f"number of inner symbols: {k}; index symbols: {self.index_symbols}")
216
+
182
217
  logging.debug( "strand capacity in bytes: {}".format(self.data_chunk_size) )
218
+ #todo: only execute if logging level is debug or warning
219
+ self.__compute_waste()
183
220
 
184
- _wasted_bits = k*np.log2(q) - (self.data_chunk_size*8+self.index_bits )
185
- logging.debug( "{} bits wasted per strand".format( _wasted_bits ) )
186
- logging.debug( "Maximum strand index (using wasted bits): {}".format(2**(int(self.index_bits+_wasted_bits))))
187
- _wasted_symbols = _wasted_bits/np.log2(q)
188
- if _wasted_symbols>=1:
189
- logging.warning( "{} symbols wasted per strand. Consider decreasing inner_n.".format( _wasted_symbols) )
190
- else:
191
- logging.debug("{} symbols wasted per strand".format( _wasted_symbols))
192
-
193
-
194
221
  self.block_capacity_bytes = self.data_chunk_size*(self.n_strands - n_redundant_strands)
195
222
  logging.debug( "codec message size: {} bytes".format(self.block_capacity_bytes) )
196
223
 
197
224
  #init the outer coder
198
225
  self.outer_coder = reedsolo.RSCodec(n_redundant_strands, c_exp=8*self.outer_alphabet_size_bytes)
199
226
 
227
+ def __compute_waste(self):
228
+ k = self.inner_k
229
+ q = self.inner_coder.field.order
230
+ if self.index_type == "binary":
231
+ _wasted_bits = k*np.log2(q) - (self.data_chunk_size*8+self.index_bits )
232
+ logging.debug( "{} bits wasted per strand".format( _wasted_bits ) )
233
+ logging.debug( "Maximum strand index (using wasted bits): {}".format(2**(int(self.index_bits+_wasted_bits))))
234
+ _wasted_symbols = _wasted_bits/np.log2(q)
235
+ elif self.index_type == "inner":
236
+ data_chunk_symbols_used = self.data_chunk_size*8/np.log2(q)
237
+ _wasted_symbols = self.data_symbols - data_chunk_symbols_used
238
+ logging.debug( f"Maximum strand index:{q**self.index_symbols}")
239
+ else:
240
+ raise ValueError("Unknown index type: {}".format(self.index_type)) #should be impossible to get here.
241
+ if _wasted_symbols>=1:
242
+ logging.warning( "{} symbols wasted per strand. Consider decreasing inner_n.".format( _wasted_symbols) )
243
+ else:
244
+ logging.debug("{} symbols wasted per strand".format( _wasted_symbols))
245
+
200
246
  def encode(self,data:bytes,index_start:int=0)->list[list[int]]:
201
247
  """ Encodes the given data using the outer and inner codes.
202
248
 
@@ -231,14 +277,18 @@ class BaseNBlockCodec:
231
277
  # |<rrrrrrrrrrrrrrrrr self.data_chunk_size rrrrrrrrrrrrr>|
232
278
  # ...
233
279
  # |<rrrrrrrrrrrrrrrrr self.data_chunk_size rrrrrrrrrrrrr>|
234
- # 3. inner code (R= inner code redundancy applied to rows, * = inner code symbol)
235
- # |<***************** self.data_chunk_size *************RRRRRRRR>|
236
- # |<***************** self.data_chunk_size *************RRRRRRRR>|
280
+ # 3. inner code (R= inner code redundancy applied to rows)
281
+ # (* = inner code symbol)
282
+ # (@= binary index concatenated to data bits)
283
+ # (a= binary index concatenated to outer code redundancy bits)
284
+ # When index type is inner code, @ and a are full symbols.
285
+ # |<***************** self.data_chunk_size *************@@@RRRRRRRR>|
286
+ # |<***************** self.data_chunk_size *************@@@RRRRRRRR>|
237
287
  # ...
238
- # |<***************** self.data_chunk_size *************RRRRRRRR>|
239
- # |<rrrrrrrrrrrrrrrrr self.data_chunk_size rrrrrrrrrrrrrRRRRRRRR>|
288
+ # |<***************** self.data_chunk_size *************@@@RRRRRRRR>|
289
+ # |<rrrrrrrrrrrrrrrrr self.data_chunk_size rrrrrrrrrrrrraaaRRRRRRRR>|
240
290
  # ...
241
- # |<rrrrrrrrrrrrrrrrr self.data_chunk_size rrrrrrrrrrrrrRRRRRRRR>|
291
+ # |<rrrrrrrrrrrrrrrrr self.data_chunk_size rrrrrrrrrrrrraaaRRRRRRRR>|
242
292
 
243
293
  #1. Reshape data
244
294
  logging.debug("applying outer code to {} bytes".format(len(data)))
@@ -273,11 +323,23 @@ class BaseNBlockCodec:
273
323
  d = chunk.tobytes()
274
324
  #use little endian to match x86/x64 byte order and avoid needing to pad byte string.
275
325
  chunk = int.from_bytes(d,'little')
276
- #add index to end (MS bits)
277
- chunk += index<<(self.data_chunk_size*8)
326
+ if self.index_type == "binary":
327
+ #add index to end (MS bits)
328
+ chunk += index<<(self.data_chunk_size*8)
329
+
330
+ #but.. we still need to pad out the symbol stream.
331
+ chunk = _int_to_baseN(chunk,self.inner_coder.field.order,length=self.inner_k)
332
+ elif self.index_type == "inner":
333
+ chunk = _int_to_baseN(chunk,self.inner_coder.field.order,length=self.data_symbols)
334
+ index_symbols = _int_to_baseN(index,self.inner_coder.field.order,length=self.index_symbols)
335
+ if self.index_location == "middle":
336
+ chunk += index_symbols
337
+ elif self.index_location == "beginning":
338
+ chunk = index_symbols + chunk
339
+ else:
340
+ raise NotImplementedError("index_location 'end' is not implemented.")
341
+
278
342
  index += 1
279
- #but.. we still need to pad out the symbol stream.
280
- chunk = _int_to_baseN(chunk,self.inner_coder.field.order,length=self.inner_k)
281
343
  chunks.append(chunk)
282
344
 
283
345
  #apply inner code
@@ -313,12 +375,23 @@ class BaseNBlockCodec:
313
375
  ordered_chunks:list[bytes|None] = [None] * self.n_strands
314
376
  chunk_errors:list[int] = [-1] * self.n_strands
315
377
  for chunk in chunked_data:
316
- chunk_int = _baseN_to_int(chunk[0].tolist(), self.inner_coder.field.order)
317
- chunk_index = (chunk_int >> (self.data_chunk_size * 8)) - index_start
318
- _mask = (2 ** (self.data_chunk_size * 8)) - 1
319
- chunk_int = chunk_int & _mask
378
+ if self.index_type == "binary":
379
+ chunk_int = _baseN_to_int(chunk[0].tolist(), self.inner_coder.field.order)
380
+ chunk_index = (chunk_int >> (self.data_chunk_size * 8)) - index_start
381
+ _mask = (2 ** (self.data_chunk_size * 8)) - 1
382
+ chunk_int = chunk_int & _mask
383
+ elif self.index_type == "inner":
384
+ if self.index_location == "middle":
385
+ chunk_int = _baseN_to_int(chunk[0][:self.data_symbols].tolist(),self.inner_coder.field.order)
386
+ chunk_index = _baseN_to_int(chunk[0][self.data_symbols:].tolist(),self.inner_coder.field.order) - index_start
387
+ elif self.index_location == "beginning":
388
+ chunk_index = _baseN_to_int(chunk[0][:-self.data_symbols].tolist(),self.inner_coder.field.order) - index_start
389
+ chunk_int = _baseN_to_int(chunk[0][-self.data_symbols:].tolist(),self.inner_coder.field.order)
390
+ else:
391
+ raise NotImplementedError("index_location 'end' is not implemented.")
392
+ else:
393
+ raise ValueError("Unknown index type: {}".format(self.index_type)) #should be impossible to get here.
320
394
  chunk_bytes:bytes = chunk_int.to_bytes(self.data_chunk_size, 'little')
321
- #print(chunk_index)
322
395
  try:
323
396
  ordered_chunks[chunk_index] = chunk_bytes
324
397
  chunk_errors[chunk_index] = chunk[1]
@@ -381,27 +454,26 @@ class BaseNBlockCodec:
381
454
  Returns:
382
455
  int: The index. or -1 if error_check==True and unrecoverable error found.
383
456
  """
384
- word_len:int = len(words[0])
457
+ symbol_len:int = len(words[0])
385
458
  if isinstance(dna,bytes):
386
459
  dna= dna.decode()
387
460
  dna_s:str = cast(str,dna)
388
-
389
- if padding == "right":
390
- _p = self.inner_k*word_len - len(dna_s)
391
- if _p>0: #if i have less than a full codeword of bases
392
- dna_s = dna_s + "A"*_p
393
- elif padding == "left":
394
- _p = self.inner_k*word_len - len(dna_s)
395
- if _p>0: #if i have less than a full codeword of bases
396
- dna_s = "A"*_p + dna_s
397
461
 
462
+ num_missing = self.inner_k*symbol_len - len(dna_s) # in DNA bases
463
+ if num_missing>0: #if i have less than a full codeword of bases
464
+ if padding == "right":
465
+ dna_s = dna_s + "A"*num_missing
466
+ elif padding == "left":
467
+ dna_s = "A"*num_missing + dna_s
468
+
469
+ #This could fail if padding makes a symbol not in words or alternate_words.
398
470
  data = dna_to_bN([dna_s],words,alternate_words)[0]
399
471
 
400
472
  return self.extract_index(data,error_check)
401
473
 
402
474
  def extract_index(self, data: list[int], error_check: bool = False) -> int:
403
475
  """
404
- Extracts the index from the given DNA strand.
476
+ Extracts the index from the given inner code sequence.
405
477
 
406
478
  Args:
407
479
  data list[int]: The input data from which to extract the index.
@@ -415,11 +487,23 @@ class BaseNBlockCodec:
415
487
  if error_check == False:
416
488
  data = data[0:self.inner_k] #extract message
417
489
  else:
418
- data, n_corrected = self.inner_coder.decode(data,errors=True)[0].tolist()
490
+ data_gf, n_corrected = self.inner_coder.decode(data,errors=True)
491
+ data = data_gf.tolist()
419
492
  if n_corrected < 0:
420
493
  return -1
421
- chunk_int = _baseN_to_int(data, self.inner_coder.field.order)
422
- chunk_index = (chunk_int >> (self.data_chunk_size * 8))
494
+ if self.index_type == "binary":
495
+ chunk_int = _baseN_to_int(data, self.inner_coder.field.order)
496
+ chunk_index = (chunk_int >> (self.data_chunk_size * 8))
497
+ elif self.index_type == "inner":
498
+ if self.index_location == "middle":
499
+ chunk_index_baseN = data[self.data_symbols:]
500
+ elif self.index_location == "beginning":
501
+ chunk_index_baseN = data[:-self.data_symbols]
502
+ else:
503
+ raise NotImplementedError("index_location 'end' is not implemented.")
504
+ chunk_index = _baseN_to_int(chunk_index_baseN, self.inner_coder.field.order)
505
+ else:
506
+ raise ValueError("Unknown index type: {}".format(self.index_type)) #should be impossible to get here.
423
507
  return chunk_index
424
508
 
425
509
 
@@ -3,6 +3,7 @@
3
3
  import random
4
4
  import numpy as np
5
5
  from itertools import product
6
+ from typing import Literal
6
7
 
7
8
  import syncoder
8
9
  codec = syncoder
@@ -12,11 +13,27 @@ codec = syncoder
12
13
  #numba_logger.setLevel(logging.WARNING)
13
14
 
14
15
  def test_encode_decode():
15
- c = codec.BaseNBlockCodec(inner_alphabet_size=32,inner_d=5,inner_n=30)
16
+ encode_decode(index_type="binary")
17
+ encode_decode(index_type="inner")
18
+ encode_decode(index_type="inner", index_loc="beginning")
19
+
20
+ def encode_decode(index_type,index_loc:Literal["beginning","middle"]="middle"):
21
+ c = codec.BaseNBlockCodec(inner_alphabet_size=32,
22
+ inner_d=5,
23
+ inner_n=30,
24
+ index_type=index_type,
25
+ index_location=index_loc)
16
26
 
17
27
  in_text = (codec.lipsum + codec.lipsum)[0:c.block_capacity_bytes]
18
28
 
19
29
  coded = c.encode( in_text )
30
+ assert c.extract_index(coded[27]) == 27 #test extract index while we're at it
31
+ dnadata = syncoder.b32_to_DNA(coded,syncoder._default_b32_alphabet,syncoder._default_b32_alphabet_alt)
32
+ assert c.extract_index_dna(dnadata[27],
33
+ words = syncoder._default_b32_alphabet,
34
+ alternate_words= syncoder._default_b32_alphabet_alt,
35
+ error_check=True) == 27
36
+
20
37
  random.shuffle(coded)
21
38
  #coded = coded[:-4] #erase a random strands
22
39
  corrupt_index = random.randint(0,len(coded[0]))
@@ -76,6 +93,7 @@ def test_dna_to_bytes():
76
93
  dna_bytes, mask = codec.dna_to_bytes(DNA,alphabet,alphabet_alt)
77
94
 
78
95
  dna_baseN = _int_to_baseN(int.from_bytes(dna_bytes,"little"),len(alphabet))
96
+ #TODO: this can fail but only for some inputs randomly selected lookinto why.
79
97
  assert dna_baseN == answer32
80
98
 
81
99
  _t = np.array(dna_baseN)
@@ -1,12 +0,0 @@
1
- name: codec-user
2
- channels:
3
- - conda-forge
4
- dependencies:
5
- - python>=3.12
6
- - numpy=2.*
7
- - pytest
8
- - reedsolo=1.7.*
9
- - pip=24.*
10
- - pip:
11
- - galois==0.4.*
12
-
@@ -1,20 +0,0 @@
1
- name: codec-dev
2
- channels:
3
- - conda-forge
4
- dependencies:
5
- - python>=3.12
6
- - numpy=2.*
7
- - matplotlib=3.*
8
- - pytest
9
- - reedsolo=1.7.*
10
- - pip=24.*
11
- - sphinx=7.*
12
- - sphinx_rtd_theme
13
- - biopython
14
- - jupyter
15
- - ipykernel
16
- - pip:
17
- - galois==0.4.*
18
- - build
19
- - twine
20
-
@@ -1,14 +0,0 @@
1
- name: codec-test
2
- channels:
3
- - conda-forge
4
- dependencies:
5
- - python>=3.12
6
- - numpy=2.*
7
- - pytest
8
- - reedsolo=1.7.*
9
- - pip=24.*
10
- - sphinx=7.*
11
- - sphinx_rtd_theme
12
- - pip:
13
- - galois==0.4.*
14
-
File without changes
File without changes
File without changes
File without changes