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.
- {syncoder-0.0.1a1 → syncoder-0.0.3a1}/.gitignore +2 -0
- {syncoder-0.0.1a1 → syncoder-0.0.3a1}/PKG-INFO +4 -1
- {syncoder-0.0.1a1 → syncoder-0.0.3a1}/environments/conda_environment.yml +1 -1
- {syncoder-0.0.1a1 → syncoder-0.0.3a1}/environments/conda_environment_dev.yml +1 -1
- {syncoder-0.0.1a1 → syncoder-0.0.3a1}/environments/conda_environment_test.yml +1 -1
- {syncoder-0.0.1a1 → syncoder-0.0.3a1}/pyproject.toml +10 -2
- {syncoder-0.0.1a1 → syncoder-0.0.3a1}/src/syncoder/__init__.py +3 -1
- {syncoder-0.0.1a1 → syncoder-0.0.3a1}/src/syncoder/codec.py +38 -18
- {syncoder-0.0.1a1 → syncoder-0.0.3a1}/tests/test_codec_pytest.py +23 -4
- {syncoder-0.0.1a1 → syncoder-0.0.3a1}/.vscode/.gitignore +0 -0
- {syncoder-0.0.1a1 → syncoder-0.0.3a1}/LICENSE +0 -0
- {syncoder-0.0.1a1 → syncoder-0.0.3a1}/README.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: syncoder
|
|
3
|
-
Version: 0.0.
|
|
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
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "syncoder"
|
|
3
|
-
|
|
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
|
|
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
|
|
|
@@ -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(
|
|
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.
|
|
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
|
|
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
|
|
File without changes
|