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.
- {syncoder-0.0.2a1 → syncoder-0.0.4a1}/.gitignore +1 -0
- {syncoder-0.0.2a1 → syncoder-0.0.4a1}/PKG-INFO +1 -1
- {syncoder-0.0.2a1 → syncoder-0.0.4a1}/src/syncoder/__init__.py +1 -1
- {syncoder-0.0.2a1 → syncoder-0.0.4a1}/src/syncoder/codec.py +94 -17
- {syncoder-0.0.2a1 → syncoder-0.0.4a1}/tests/test_codec_pytest.py +33 -2
- {syncoder-0.0.2a1 → syncoder-0.0.4a1}/.vscode/.gitignore +0 -0
- {syncoder-0.0.2a1 → syncoder-0.0.4a1}/LICENSE +0 -0
- {syncoder-0.0.2a1 → syncoder-0.0.4a1}/README.md +0 -0
- {syncoder-0.0.2a1 → syncoder-0.0.4a1}/environments/conda_environment.yml +0 -0
- {syncoder-0.0.2a1 → syncoder-0.0.4a1}/environments/conda_environment_dev.yml +0 -0
- {syncoder-0.0.2a1 → syncoder-0.0.4a1}/environments/conda_environment_test.yml +0 -0
- {syncoder-0.0.2a1 → syncoder-0.0.4a1}/pyproject.toml +0 -0
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
73
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|