syncoder 0.0.4a1__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.4a1
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.4a1"
6
+ __version__ = "0.0.5a0"
@@ -10,7 +10,15 @@ import numpy as np
10
10
  from numpy.typing import ArrayLike
11
11
 
12
12
  import galois
13
- import reedsolo
13
+
14
+ #import a version of reedsolo that supports multi instance
15
+ import importlib.metadata
16
+ import packaging.version
17
+ rsvers = importlib.metadata.version('reedsolo')
18
+ if packaging.version.Version(rsvers) <= packaging.version.Version("2.1.3b1"):
19
+ from . import reedsoloMulti as reedsolo
20
+ else:
21
+ import reedsolo
14
22
 
15
23
  #### ROADMAP ####
16
24
  # - integrate the baseN to DNA function here. (generalize from b32fn)
@@ -116,7 +124,9 @@ class BaseNBlockCodec:
116
124
  inner_n:Optional[int]=None,
117
125
  n_redundant_strands:int=5,
118
126
  n_strands:int=100,
119
- 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"):
120
130
  """
121
131
  Initialize the Codec object.
122
132
 
@@ -127,6 +137,16 @@ class BaseNBlockCodec:
127
137
  n_redundant_strands (int): Number of redundant strands.
128
138
  n_strands (int): Number of "strands" stored by this codec including redundant strands.
129
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)
130
150
  """
131
151
  if n_strands<=255:
132
152
  self.outer_alphabet_size_bytes = 1
@@ -139,6 +159,20 @@ class BaseNBlockCodec:
139
159
  max_strand_index = n_strands
140
160
  if max_strand_index<n_strands:
141
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'.")
142
176
 
143
177
  self.n_message_strands = n_strands - n_redundant_strands
144
178
 
@@ -170,25 +204,45 @@ class BaseNBlockCodec:
170
204
  logging.debug("bits per strand used for indexing: {}".format(self.index_bits))
171
205
 
172
206
  #message length in bytes (without index)
173
- 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
+
174
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()
175
220
 
176
- _wasted_bits = k*np.log2(q) - (self.data_chunk_size*8+self.index_bits )
177
- logging.debug( "{} bits wasted per strand".format( _wasted_bits ) )
178
- logging.debug( "Maximum strand index (using wasted bits): {}".format(2**(int(self.index_bits+_wasted_bits))))
179
- _wasted_symbols = _wasted_bits/np.log2(q)
180
- if _wasted_symbols>=1:
181
- logging.warning( "{} symbols wasted per strand. Consider decreasing inner_n.".format( _wasted_symbols) )
182
- else:
183
- logging.debug("{} symbols wasted per strand".format( _wasted_symbols))
184
-
185
-
186
221
  self.block_capacity_bytes = self.data_chunk_size*(self.n_strands - n_redundant_strands)
187
222
  logging.debug( "codec message size: {} bytes".format(self.block_capacity_bytes) )
188
223
 
189
224
  #init the outer coder
190
225
  self.outer_coder = reedsolo.RSCodec(n_redundant_strands, c_exp=8*self.outer_alphabet_size_bytes)
191
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
+
192
246
  def encode(self,data:bytes,index_start:int=0)->list[list[int]]:
193
247
  """ Encodes the given data using the outer and inner codes.
194
248
 
@@ -223,14 +277,18 @@ class BaseNBlockCodec:
223
277
  # |<rrrrrrrrrrrrrrrrr self.data_chunk_size rrrrrrrrrrrrr>|
224
278
  # ...
225
279
  # |<rrrrrrrrrrrrrrrrr self.data_chunk_size rrrrrrrrrrrrr>|
226
- # 3. inner code (R= inner code redundancy applied to rows, * = inner code symbol)
227
- # |<***************** self.data_chunk_size *************RRRRRRRR>|
228
- # |<***************** 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>|
229
287
  # ...
230
- # |<***************** self.data_chunk_size *************RRRRRRRR>|
231
- # |<rrrrrrrrrrrrrrrrr self.data_chunk_size rrrrrrrrrrrrrRRRRRRRR>|
288
+ # |<***************** self.data_chunk_size *************@@@RRRRRRRR>|
289
+ # |<rrrrrrrrrrrrrrrrr self.data_chunk_size rrrrrrrrrrrrraaaRRRRRRRR>|
232
290
  # ...
233
- # |<rrrrrrrrrrrrrrrrr self.data_chunk_size rrrrrrrrrrrrrRRRRRRRR>|
291
+ # |<rrrrrrrrrrrrrrrrr self.data_chunk_size rrrrrrrrrrrrraaaRRRRRRRR>|
234
292
 
235
293
  #1. Reshape data
236
294
  logging.debug("applying outer code to {} bytes".format(len(data)))
@@ -265,11 +323,23 @@ class BaseNBlockCodec:
265
323
  d = chunk.tobytes()
266
324
  #use little endian to match x86/x64 byte order and avoid needing to pad byte string.
267
325
  chunk = int.from_bytes(d,'little')
268
- #add index to end (MS bits)
269
- 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
+
270
342
  index += 1
271
- #but.. we still need to pad out the symbol stream.
272
- chunk = _int_to_baseN(chunk,self.inner_coder.field.order,length=self.inner_k)
273
343
  chunks.append(chunk)
274
344
 
275
345
  #apply inner code
@@ -305,12 +375,23 @@ class BaseNBlockCodec:
305
375
  ordered_chunks:list[bytes|None] = [None] * self.n_strands
306
376
  chunk_errors:list[int] = [-1] * self.n_strands
307
377
  for chunk in chunked_data:
308
- chunk_int = _baseN_to_int(chunk[0].tolist(), self.inner_coder.field.order)
309
- chunk_index = (chunk_int >> (self.data_chunk_size * 8)) - index_start
310
- _mask = (2 ** (self.data_chunk_size * 8)) - 1
311
- 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.
312
394
  chunk_bytes:bytes = chunk_int.to_bytes(self.data_chunk_size, 'little')
313
- #print(chunk_index)
314
395
  try:
315
396
  ordered_chunks[chunk_index] = chunk_bytes
316
397
  chunk_errors[chunk_index] = chunk[1]
@@ -373,27 +454,26 @@ class BaseNBlockCodec:
373
454
  Returns:
374
455
  int: The index. or -1 if error_check==True and unrecoverable error found.
375
456
  """
376
- word_len:int = len(words[0])
457
+ symbol_len:int = len(words[0])
377
458
  if isinstance(dna,bytes):
378
459
  dna= dna.decode()
379
460
  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
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.
390
470
  data = dna_to_bN([dna_s],words,alternate_words)[0]
391
471
 
392
472
  return self.extract_index(data,error_check)
393
473
 
394
474
  def extract_index(self, data: list[int], error_check: bool = False) -> int:
395
475
  """
396
- Extracts the index from the given DNA strand.
476
+ Extracts the index from the given inner code sequence.
397
477
 
398
478
  Args:
399
479
  data list[int]: The input data from which to extract the index.
@@ -407,11 +487,23 @@ class BaseNBlockCodec:
407
487
  if error_check == False:
408
488
  data = data[0:self.inner_k] #extract message
409
489
  else:
410
- 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()
411
492
  if n_corrected < 0:
412
493
  return -1
413
- chunk_int = _baseN_to_int(data, self.inner_coder.field.order)
414
- 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.
415
507
  return chunk_index
416
508
 
417
509