syncoder 0.0.0__py3-none-any.whl
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.
Potentially problematic release.
This version of syncoder might be problematic. Click here for more details.
- syncoder/__init__.py +1 -0
- syncoder/codec.py +618 -0
- syncoder-0.0.0.dist-info/METADATA +13 -0
- syncoder-0.0.0.dist-info/RECORD +6 -0
- syncoder-0.0.0.dist-info/WHEEL +4 -0
- syncoder-0.0.0.dist-info/licenses/LICENSE +9 -0
syncoder/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__all__ = ["bar.nextfn"]
|
syncoder/codec.py
ADDED
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import struct
|
|
3
|
+
import itertools
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Optional,Union, Callable #requires python 3.5 or later
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from numpy.typing import ArrayLike
|
|
10
|
+
|
|
11
|
+
import galois
|
|
12
|
+
import reedsolo
|
|
13
|
+
|
|
14
|
+
#### ROADMAP ####
|
|
15
|
+
# - integrate the baseN to DNA function here. (generalize from b32fn)
|
|
16
|
+
# - Strand prefix sub-encoder.
|
|
17
|
+
# - Sample objective functions (at least minimize homopolymers and avoid annealing.)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
lipsum = b"Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra nulla ut metus varius laoreet. Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet adipiscing sem neque sed ipsum." #cspell:disable-line
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _int_to_baseN(n:int,base:int,length:int=-1)->list[int]:
|
|
24
|
+
"""Converts an integer to a base N number in little endian order.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
n (int): The integer to be converted.
|
|
28
|
+
base (int): The base of the number system.
|
|
29
|
+
length (int, optional): If specified, the number will be zero padded to this length.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
list[int]: The list of digits representing the base N number.
|
|
33
|
+
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
digits = []
|
|
37
|
+
while n>0:
|
|
38
|
+
n,r = divmod(n,base)
|
|
39
|
+
digits.append(r)
|
|
40
|
+
|
|
41
|
+
#do zero padding
|
|
42
|
+
while len(digits)<length:
|
|
43
|
+
digits.append(0)
|
|
44
|
+
return digits
|
|
45
|
+
|
|
46
|
+
def _baseN_to_int(digits: list[int], base: int) -> int:
|
|
47
|
+
"""
|
|
48
|
+
Converts a base N number to an integer using little endian order.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
digits (list[int]): The list of digits representing the base N number.
|
|
52
|
+
base (int): The base of the number.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
int: The converted integer value.
|
|
56
|
+
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
n = 0
|
|
60
|
+
for d in reversed(digits):
|
|
61
|
+
n = n*base + d
|
|
62
|
+
return n
|
|
63
|
+
|
|
64
|
+
def computeNStrands(nbytes:int,
|
|
65
|
+
inner_alphabet_size:int=32,
|
|
66
|
+
inner_d:int=5,
|
|
67
|
+
inner_n:Optional[int]=None,
|
|
68
|
+
n_redundant_strands:int=5)->int:
|
|
69
|
+
|
|
70
|
+
#TODO: do some algebra to solve for n_strands.
|
|
71
|
+
min_m=1
|
|
72
|
+
max_m=2**16-1
|
|
73
|
+
while min_m<max_m:
|
|
74
|
+
mid = (min_m+max_m)//2
|
|
75
|
+
codec = BaseNBlockCodec(inner_alphabet_size=inner_alphabet_size,
|
|
76
|
+
inner_d=inner_d,
|
|
77
|
+
inner_n=inner_n,
|
|
78
|
+
n_redundant_strands=n_redundant_strands,
|
|
79
|
+
n_strands=mid)
|
|
80
|
+
if codec.block_capacity_bytes < nbytes:
|
|
81
|
+
min_m = mid+1
|
|
82
|
+
else:
|
|
83
|
+
max_m = mid
|
|
84
|
+
return min_m
|
|
85
|
+
#index_bits = np.ceil(np.log2(n_strands)).astype(int)
|
|
86
|
+
#k = inner_n - (inner_d - 1)
|
|
87
|
+
#data_chunk_size = int(k*np.log2(inner_alphabet_size)-index_bits) // 8
|
|
88
|
+
#block_capacity_bytes = data_chunk_size*(n_strands - n_redundant_strands)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class BaseNBlockCodec:
|
|
94
|
+
def __init__(self,
|
|
95
|
+
inner_alphabet_size:int=32,
|
|
96
|
+
inner_d:int=5,
|
|
97
|
+
inner_n:Optional[int]=None,
|
|
98
|
+
n_redundant_strands:int=5,
|
|
99
|
+
n_strands:int=100,
|
|
100
|
+
max_strand_index:Optional[int]=None):
|
|
101
|
+
"""
|
|
102
|
+
Initialize the Codec object.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
inner_alphabet_size (int): Size of the inner alphabet (must be a prime power).
|
|
106
|
+
inner_d (int): Inner codeword distance.
|
|
107
|
+
inner_n (Optional[int]): Number of symbols in inner block (n < inner_alphabet_size). Default: inner_alphabet_size-1.
|
|
108
|
+
n_redundant_strands (int): Number of redundant strands.
|
|
109
|
+
n_strands (int): Number of "strands" stored by this codec including redundant strands.
|
|
110
|
+
max_strand_index (Optional[int]): Maximum guaranteed index. Default: n_strands.
|
|
111
|
+
"""
|
|
112
|
+
#TODO: handle more than 255 total strands.
|
|
113
|
+
#this was formally a parameter but probably better to keep it fixed or automatically calculated.
|
|
114
|
+
if n_strands<=255:
|
|
115
|
+
self.outer_alphabet_size_bytes = 1
|
|
116
|
+
else:
|
|
117
|
+
self.outer_alphabet_size_bytes = 2
|
|
118
|
+
if n_strands>(2**(8*self.outer_alphabet_size_bytes)-1):
|
|
119
|
+
raise ValueError("Too many strands for outer code.")
|
|
120
|
+
self.n_strands = n_strands
|
|
121
|
+
|
|
122
|
+
self.n_message_strands = n_strands - n_redundant_strands
|
|
123
|
+
|
|
124
|
+
if inner_n is None:
|
|
125
|
+
inner_n = inner_alphabet_size - 1
|
|
126
|
+
else:
|
|
127
|
+
#TODO: if inner_n is requested to be too large, double up on inner blocks for a single "strand"
|
|
128
|
+
# make sure extra sequence numbers are not added to inner blocks. and or maybe add a
|
|
129
|
+
# parameter for the number of inner blocks per strand.
|
|
130
|
+
assert inner_n<inner_alphabet_size
|
|
131
|
+
|
|
132
|
+
self.inner_coder = galois.ReedSolomon(n=inner_alphabet_size-1,d=inner_d,field=galois.GF(inner_alphabet_size))
|
|
133
|
+
# in units of symbols (there are q=inner_alphabet_size possible symbols)
|
|
134
|
+
# ##################strand (n symbols)#######################
|
|
135
|
+
# # message # index # redundancy #
|
|
136
|
+
# |<----------------- k ------------------->|<---- d-1 ---->|
|
|
137
|
+
# |<----------------------- n ---------------------------->|
|
|
138
|
+
#
|
|
139
|
+
# sizes in bytes:
|
|
140
|
+
# |<---- self.data_chunk_size ----->|<- . ->|
|
|
141
|
+
# ^ = self.index_bits <-- in bits not bytes.
|
|
142
|
+
# note: actual k remains alphabet_size-1, but the code is shorted (0 padded) to inner_n symbols.
|
|
143
|
+
k = inner_n - (inner_d - 1)
|
|
144
|
+
q = self.inner_coder.field.order
|
|
145
|
+
|
|
146
|
+
#number of bytes needed to store the index
|
|
147
|
+
self.index_bits = np.ceil(np.log2(self.n_strands)).astype(int)
|
|
148
|
+
logging.debug("bits per strand used for indexing: {}".format(self.index_bits))
|
|
149
|
+
|
|
150
|
+
#message length in bytes (without index)
|
|
151
|
+
self.data_chunk_size = int(k*np.log2(q)-self.index_bits) // 8
|
|
152
|
+
logging.debug( "strand capacity in bytes: {}".format(self.data_chunk_size) )
|
|
153
|
+
|
|
154
|
+
_wasted_bits = k*np.log2(q) - (self.data_chunk_size*8+self.index_bits )
|
|
155
|
+
logging.debug( "{} bits wasted per strand".format( _wasted_bits ) )
|
|
156
|
+
logging.debug( "Maximum strand index (using wasted bits): {}".format(2**(int(self.index_bits+_wasted_bits))))
|
|
157
|
+
_wasted_symbols = _wasted_bits/np.log2(q)
|
|
158
|
+
if _wasted_symbols>=1:
|
|
159
|
+
logging.warning( "{} symbols wasted per strand. Consider decreasing inner_n.".format( _wasted_symbols) )
|
|
160
|
+
else:
|
|
161
|
+
logging.debug("{} symbols wasted per strand".format( _wasted_symbols))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
self.block_capacity_bytes = self.data_chunk_size*(self.n_strands - n_redundant_strands)
|
|
165
|
+
logging.debug( "codec message size: {} bytes".format(self.block_capacity_bytes) )
|
|
166
|
+
|
|
167
|
+
#init the outer coder
|
|
168
|
+
self.outer_coder = reedsolo.RSCodec(n_redundant_strands, c_exp=8*self.outer_alphabet_size_bytes)
|
|
169
|
+
|
|
170
|
+
def encode(self,data:bytes,index_start:int=0)->list[list[int]]:
|
|
171
|
+
""" Encodes the given data using the outer and inner codes.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
data (bytes): The input data to be encoded.
|
|
175
|
+
index_start (int, optional): The starting index for the encoded chunks. Defaults to 0.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
list[list[int]]: The encoded chunks of data.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
#TODO: add option to put the index in multiples of inner symbols instead of bytes.
|
|
182
|
+
# this would give options to improve data packing depending on parameters.
|
|
183
|
+
# even better this could be autocomputed.
|
|
184
|
+
#TODO: consider putting outer code redundancy striped vertically bytewise so that
|
|
185
|
+
# lost strands with odd numbers of bytes don't erase bytes from neighboring strands.
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
#encode process outline:
|
|
189
|
+
# 0. serial data
|
|
190
|
+
# |--------------------------------... -----------|
|
|
191
|
+
# 1. strand length payload chunks (self.data_chunk_size in bytes)
|
|
192
|
+
# |<----------------- self.data_chunk_size ------------->|
|
|
193
|
+
# |<----------------- self.data_chunk_size ------------->|
|
|
194
|
+
# ...
|
|
195
|
+
# |<----------------- self.data_chunk_size ------------->|
|
|
196
|
+
# 2. outer code (r= outer code redundancy applied to columns)
|
|
197
|
+
# |<----------------- self.data_chunk_size ------------->|
|
|
198
|
+
# |<----------------- self.data_chunk_size ------------->|
|
|
199
|
+
# ...
|
|
200
|
+
# |<----------------- self.data_chunk_size ------------->|
|
|
201
|
+
# |<rrrrrrrrrrrrrrrrr self.data_chunk_size rrrrrrrrrrrrr>|
|
|
202
|
+
# ...
|
|
203
|
+
# |<rrrrrrrrrrrrrrrrr self.data_chunk_size rrrrrrrrrrrrr>|
|
|
204
|
+
# 3. inner code (R= inner code redundancy applied to rows, * = inner code symbol)
|
|
205
|
+
# |<***************** self.data_chunk_size *************RRRRRRRR>|
|
|
206
|
+
# |<***************** self.data_chunk_size *************RRRRRRRR>|
|
|
207
|
+
# ...
|
|
208
|
+
# |<***************** self.data_chunk_size *************RRRRRRRR>|
|
|
209
|
+
# |<rrrrrrrrrrrrrrrrr self.data_chunk_size rrrrrrrrrrrrrRRRRRRRR>|
|
|
210
|
+
# ...
|
|
211
|
+
# |<rrrrrrrrrrrrrrrrr self.data_chunk_size rrrrrrrrrrrrrRRRRRRRR>|
|
|
212
|
+
|
|
213
|
+
#1. Reshape data
|
|
214
|
+
logging.debug("applying outer code to {} bytes".format(len(data)))
|
|
215
|
+
# reshape data so columns can be coded independently.
|
|
216
|
+
if self.outer_coder.c_exp==8:
|
|
217
|
+
data_np = np.frombuffer(data,dtype=np.uint8)
|
|
218
|
+
assert len(data_np)%self.data_chunk_size == 0
|
|
219
|
+
data_np = data_np.reshape(-1,self.data_chunk_size)
|
|
220
|
+
elif self.outer_coder.c_exp==16:
|
|
221
|
+
data_np = np.frombuffer(data,dtype=np.uint16)
|
|
222
|
+
if self.data_chunk_size%2!=0:
|
|
223
|
+
raise ValueError("data_chunk_size must be even for 16 bit outer code.")
|
|
224
|
+
if len(data_np)%(self.data_chunk_size//2) != 0:
|
|
225
|
+
raise ValueError(f"data length(={len(data_np)}) must be multiple of data_chunk_size//2(={self.data_chunk_size//2})."+
|
|
226
|
+
" Consider padding data.")
|
|
227
|
+
data_np = data_np.reshape(-1,self.data_chunk_size//2)
|
|
228
|
+
else:
|
|
229
|
+
raise ValueError("Unsupported outer code byte size.")
|
|
230
|
+
|
|
231
|
+
#2. apply the outer code (encode columns of data)
|
|
232
|
+
data_enc_cols = np.array([self.outer_coder.encode(dc) for dc in data_np.transpose()],dtype=data_np.dtype)
|
|
233
|
+
data_enc_chunks = data_enc_cols.transpose()
|
|
234
|
+
|
|
235
|
+
logging.debug("outer coded strands: {}".format(data_enc_chunks.shape[0]))
|
|
236
|
+
|
|
237
|
+
#3. apply the inner code
|
|
238
|
+
chunks = []
|
|
239
|
+
index = index_start
|
|
240
|
+
_temp = 0
|
|
241
|
+
for chunk in data_enc_chunks:
|
|
242
|
+
#add index to "strand".
|
|
243
|
+
d = chunk.tobytes()
|
|
244
|
+
#use little endian to match x86/x64 byte order and avoid needing to pad byte string.
|
|
245
|
+
chunk = int.from_bytes(d,'little')
|
|
246
|
+
#add index to end (MS bits)
|
|
247
|
+
chunk += index<<(self.data_chunk_size*8)
|
|
248
|
+
index += 1
|
|
249
|
+
#but.. we still need to pad out the symbol stream.
|
|
250
|
+
chunk = _int_to_baseN(chunk,self.inner_coder.field.order,length=self.inner_coder.k)
|
|
251
|
+
chunks.append(chunk)
|
|
252
|
+
|
|
253
|
+
#apply inner code
|
|
254
|
+
ic_chunks= [self.inner_coder.encode(c).tolist() for c in chunks]
|
|
255
|
+
|
|
256
|
+
return ic_chunks
|
|
257
|
+
|
|
258
|
+
def decode(self, data: list[list[int]], index_start: int = 0) -> bytes:
|
|
259
|
+
"""
|
|
260
|
+
Decodes the given data.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
data (list[list[int]]): The encoded data to be decoded.
|
|
264
|
+
index_start (int, optional): The starting index for decoding. Defaults to 0.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
bytes: The decoded data.
|
|
268
|
+
outer erasures: list of erasures in outer code.
|
|
269
|
+
outer errors: list of errors in outer code.
|
|
270
|
+
inner errors: list of number of errors in inner code by strand index. -1 -> strand not present.
|
|
271
|
+
|
|
272
|
+
Raises:
|
|
273
|
+
None
|
|
274
|
+
"""
|
|
275
|
+
# inner decode
|
|
276
|
+
## TODO: handle erasures in inner code.
|
|
277
|
+
chunked_data = [self.inner_coder.decode(d,errors=True) for d in data]
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# handle outer decode
|
|
281
|
+
ordered_chunks = [None] * self.n_strands
|
|
282
|
+
chunk_errors = [-1] * self.n_strands
|
|
283
|
+
for chunk in chunked_data:
|
|
284
|
+
chunk_int = _baseN_to_int(chunk[0].tolist(), self.inner_coder.field.order)
|
|
285
|
+
chunk_index = chunk_int >> (self.data_chunk_size * 8) - index_start
|
|
286
|
+
_mask = (2 ** (self.data_chunk_size * 8)) - 1
|
|
287
|
+
chunk_int = chunk_int & _mask
|
|
288
|
+
chunk_bytes = chunk_int.to_bytes(self.data_chunk_size, 'little')
|
|
289
|
+
#print(chunk_index)
|
|
290
|
+
try:
|
|
291
|
+
ordered_chunks[chunk_index] = chunk_bytes
|
|
292
|
+
chunk_errors[chunk_index] = chunk[1]
|
|
293
|
+
except IndexError:
|
|
294
|
+
logging.info("strand index out of bounds:" + str(chunk_index))
|
|
295
|
+
|
|
296
|
+
# identify erasures
|
|
297
|
+
erasures = []
|
|
298
|
+
for i, c in enumerate(ordered_chunks):
|
|
299
|
+
if c is None:
|
|
300
|
+
erasures.append(i) # remember where erasure is
|
|
301
|
+
ordered_chunks[i] = b'\x00' * self.data_chunk_size # fill in erasures with zeros
|
|
302
|
+
|
|
303
|
+
if self.outer_coder.c_exp==8:
|
|
304
|
+
outer_dtype = np.uint8
|
|
305
|
+
elif self.outer_coder.c_exp==16:
|
|
306
|
+
outer_dtype = np.uint16
|
|
307
|
+
else:
|
|
308
|
+
raise ValueError("Unsupported outer code byte size.")
|
|
309
|
+
data_np_encoded = np.array([np.frombuffer(c, dtype=outer_dtype) for c in ordered_chunks]).transpose()
|
|
310
|
+
data_decoded_results = [self.outer_coder.decode(dc, erase_pos=erasures) for dc in data_np_encoded]
|
|
311
|
+
data_decoded_chunks = np.array([d[0] for d in data_decoded_results],dtype=outer_dtype).transpose()
|
|
312
|
+
data_decoded = data_decoded_chunks.flatten().tobytes()
|
|
313
|
+
|
|
314
|
+
# report erasures (lost strands) in outer code
|
|
315
|
+
logging.info("erasures (lost) chunks: {}".format(erasures))
|
|
316
|
+
data_errata_col_pos = [e[2] for e in data_decoded_results]
|
|
317
|
+
|
|
318
|
+
errors = []
|
|
319
|
+
for i, pos in enumerate(data_errata_col_pos):
|
|
320
|
+
for strand in pos:
|
|
321
|
+
if (not(strand in erasures)):
|
|
322
|
+
errors.append((strand, i))
|
|
323
|
+
errors.sort(key=lambda x: x[0])
|
|
324
|
+
|
|
325
|
+
# unlikely to find an error since it would have to pass the inner code.
|
|
326
|
+
for e in errors:
|
|
327
|
+
if e[0] in erasures:
|
|
328
|
+
pass
|
|
329
|
+
else:
|
|
330
|
+
logging.warning("Outer code errors found on strand {}, byte {}.".format(*e))
|
|
331
|
+
|
|
332
|
+
return data_decoded, erasures, errors, chunk_errors
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
#convert base N into DNA.
|
|
336
|
+
_default_b32_alphabet = ['GCT', 'ACT', 'AGT', 'CTC', 'TGC', 'GAG', 'GCA', 'ATG', 'AGA', 'AGC', 'CAC', 'CAT', 'CAG', 'CTG', 'TCT', 'GAC', 'GTC', 'GTG', 'ACA', 'ACG', 'ATC', 'CTA', 'CGA', 'CGT', 'TAC', 'TAG', 'TCA', 'TCG', 'TGA', 'TGT', 'GAT', 'GTA']
|
|
337
|
+
_default_b32_alphabet_alt = ['ATA', 'AAC', 'CCT', 'TAT', 'CCG', 'TGG', 'CGC', 'CGG', 'GTT', 'GGT', 'GCG', 'TCC', 'CCA', 'GGA', 'ACC', 'AGG', 'TAA', 'GCC', 'AAA', 'CCC', 'GGG', 'GGC', 'TTA', 'TTT', 'GAA', 'AAG', 'CTT', 'TTG', 'TTC', 'AAT', 'CAA', 'ATT']
|
|
338
|
+
|
|
339
|
+
def find_avoid(seq:str, avoids:list)->tuple[int]:
|
|
340
|
+
"""find index and length of avoided sequence """
|
|
341
|
+
for a in avoids:
|
|
342
|
+
loc = seq.find(a)
|
|
343
|
+
if loc>0:
|
|
344
|
+
return loc,len(a)
|
|
345
|
+
return -1,0
|
|
346
|
+
|
|
347
|
+
def longest_match(a:str,b:str):
|
|
348
|
+
def _longest_match(a:str,b:str):
|
|
349
|
+
max_match = 0
|
|
350
|
+
for i in range(len(a)):
|
|
351
|
+
match = 0
|
|
352
|
+
for offset in range(min(len(a)-i,len(b))):
|
|
353
|
+
if a[i+offset] == b[offset]:
|
|
354
|
+
match +=1
|
|
355
|
+
else:
|
|
356
|
+
match = 0
|
|
357
|
+
if match > max_match:
|
|
358
|
+
max_match = match
|
|
359
|
+
return max_match
|
|
360
|
+
A = a.upper()
|
|
361
|
+
B = b.upper()
|
|
362
|
+
return max(_longest_match(A,B),_longest_match(B,A))
|
|
363
|
+
|
|
364
|
+
#Returns the reverse complement of a DNA sequence.
|
|
365
|
+
def reverse_complement(dna_sequence: str) -> str:
|
|
366
|
+
complement = {'A': 'T', 'T': 'A', 'C': 'G', 'G': 'C', 'a': 't', 't': 'a', 'c': 'g', 'g': 'c'}
|
|
367
|
+
reversed_sequence = dna_sequence[::-1]
|
|
368
|
+
reverse_complement_sequence = ''.join(complement[nucleotide] for nucleotide in reversed_sequence)
|
|
369
|
+
return reverse_complement_sequence
|
|
370
|
+
|
|
371
|
+
def longest_binder(a:str,b:str):
|
|
372
|
+
A = reverse_complement(a)
|
|
373
|
+
return longest_match(A,b)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def b32_to_DNA_optimize(file_data:list[list[int]],words:list[str], alternate_words:list[str], mask:Union[list[int],None]=None, penalty_fn:Union[Callable[[str],int],None]=None)->list[str]:
|
|
378
|
+
"""
|
|
379
|
+
TODO: rename to bN_to_DNA_optimize, and add test.
|
|
380
|
+
mask: list of ints. 0 for word, 1 for alternate word, -1 for don't care. None for no mask.
|
|
381
|
+
penalty_fn: function that takes a DNA sequence and returns a score. penalty_fn(DNA_seq:str)->int
|
|
382
|
+
"""
|
|
383
|
+
return [b32_to_DNA_optimize_single(x,words,alternate_words,penalty_fn=penalty_fn) for x in file_data]
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def b32_to_DNA_optimize_single(strand_data:list[int],words:list[str], alternate_words:list[str], mask:Union[list[int],None]=None ,penalty_fn=None)->str:
|
|
387
|
+
"""
|
|
388
|
+
TODO: rename to bN_to_DNA_optimize_single
|
|
389
|
+
TODO: add mask support.
|
|
390
|
+
mask: list of ints. 0 for word, 1 for alternate word, -1 for don't care. None for no mask.
|
|
391
|
+
"""
|
|
392
|
+
if mask is not None:
|
|
393
|
+
mutable_ind = np.where(np.array(mask)==-1)
|
|
394
|
+
#raise NotImplementedError("mask not implemented yet")
|
|
395
|
+
else:
|
|
396
|
+
mutable_ind = np.arange(len(strand_data))
|
|
397
|
+
mask = mask.copy()
|
|
398
|
+
mask[mutable_ind] = 0
|
|
399
|
+
|
|
400
|
+
strand_data = np.array(strand_data)
|
|
401
|
+
words = np.array(list(zip(words,alternate_words)),dtype="S")
|
|
402
|
+
picks = np.zeros(len(strand_data),dtype=np.uint8)
|
|
403
|
+
picks[0:len(mask)] = mask
|
|
404
|
+
dna_seq = b"".join(words[strand_data,picks])
|
|
405
|
+
if penalty_fn is None:
|
|
406
|
+
return dna_seq,0
|
|
407
|
+
score = penalty_fn(dna_seq)
|
|
408
|
+
|
|
409
|
+
#greedy search, stop when you can't find a better solution one step away.
|
|
410
|
+
icount = 0
|
|
411
|
+
while True:
|
|
412
|
+
iteration_best_picks = picks.copy()
|
|
413
|
+
iteration_best_score = score
|
|
414
|
+
iteration_dna_seq = dna_seq
|
|
415
|
+
for i in mutable_ind:
|
|
416
|
+
new_picks = picks.copy()
|
|
417
|
+
new_picks[i] = not new_picks[i]
|
|
418
|
+
new_dna_seq = b"".join(words[strand_data,new_picks])
|
|
419
|
+
new_score = penalty_fn(new_dna_seq)
|
|
420
|
+
if new_score < iteration_best_score:
|
|
421
|
+
iteration_best_picks = new_picks
|
|
422
|
+
iteration_dna_seq = new_dna_seq
|
|
423
|
+
iteration_best_score = new_score
|
|
424
|
+
if iteration_dna_seq == dna_seq:
|
|
425
|
+
break
|
|
426
|
+
if iteration_best_score < score:
|
|
427
|
+
picks = iteration_best_picks
|
|
428
|
+
dna_seq = iteration_dna_seq
|
|
429
|
+
score = iteration_best_score
|
|
430
|
+
icount += 1
|
|
431
|
+
logging.debug(f"iteration {icount} picks: {iteration_best_picks}")
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
return dna_seq,score
|
|
435
|
+
|
|
436
|
+
def b32_to_DNA(file_data:list[list[int]],words:list[str], alternate_words:list[str], avoid_seq:list[str] = [])->list[str]:
|
|
437
|
+
""" with avoids, TODO: depricate """
|
|
438
|
+
dna = []
|
|
439
|
+
for strand in file_data:
|
|
440
|
+
_t = [words[x] for x in strand]
|
|
441
|
+
_s = "".join(_t)
|
|
442
|
+
ind,l = find_avoid(_s,avoid_seq)
|
|
443
|
+
_last_s = ""
|
|
444
|
+
while ind>0:
|
|
445
|
+
print("here")
|
|
446
|
+
if ind%3==0:
|
|
447
|
+
rep_index =ind//3
|
|
448
|
+
else:
|
|
449
|
+
rep_index = ind//3+1
|
|
450
|
+
_t[rep_index] = alternate_words[strand[rep_index]]
|
|
451
|
+
_last_s = _s
|
|
452
|
+
_s= "".join(_t)
|
|
453
|
+
if _last_s == _s:
|
|
454
|
+
raise Exception("Can't find solution")
|
|
455
|
+
|
|
456
|
+
ind,l = find_avoid(_s,avoid_seq)
|
|
457
|
+
dna.append(_s)
|
|
458
|
+
return dna
|
|
459
|
+
|
|
460
|
+
def dna_to_bN(dna: Union[list[str],list[bytes]] ,words:list[str], alternate_words:list[str])->list[int]:
|
|
461
|
+
""" takes a list of strings and converts them to a list of base N ints"""
|
|
462
|
+
|
|
463
|
+
if isinstance(dna[0],bytes):
|
|
464
|
+
dna = [x.decode() for x in dna]
|
|
465
|
+
|
|
466
|
+
wordlen = len(words[0])
|
|
467
|
+
#build the reverse lookup table
|
|
468
|
+
bNlut = {}
|
|
469
|
+
for ind in range(len(words)):
|
|
470
|
+
bNlut[words[ind]] = ind
|
|
471
|
+
bNlut[alternate_words[ind]] = ind
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
bNdatalist = []
|
|
475
|
+
for d in dna:
|
|
476
|
+
bNdata = []
|
|
477
|
+
for ind in range(0,len(d),wordlen):
|
|
478
|
+
w = d[ind:ind+wordlen]
|
|
479
|
+
bNdata.append(bNlut[w])
|
|
480
|
+
bNdatalist.append(bNdata)
|
|
481
|
+
return bNdatalist
|
|
482
|
+
|
|
483
|
+
def dna_to_b32(dna: Union[list[str],list[bytes]] ,words:list[str], alternate_words:list[str])->list[int]:
|
|
484
|
+
assert len(words) == 32
|
|
485
|
+
return dna_to_bN(dna,words,alternate_words)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def __is_pow_two(n: int):
|
|
489
|
+
"""Returns if a number is a power of two."""
|
|
490
|
+
return (n > 0) and ((n & ( ~(n-1) )) == n)
|
|
491
|
+
|
|
492
|
+
def dna_to_bytes(dna_seq: str, words: list[str]=_default_b32_alphabet, alternate_words: list[str]=_default_b32_alphabet) -> tuple[bytes, list[int]]:
|
|
493
|
+
"""Convert a DNA sequence to bytes with zero padding.
|
|
494
|
+
|
|
495
|
+
This function takes a DNA sequence and converts it into a byte representation.
|
|
496
|
+
It assumes the alphabet length and base are the same.
|
|
497
|
+
The function ensures that the DNA sequence is properly decoded into bytes, with
|
|
498
|
+
zero padding applied as necessary to align the data.
|
|
499
|
+
|
|
500
|
+
Args:
|
|
501
|
+
dna_seq (str): The DNA sequence to be converted.
|
|
502
|
+
words (list[str]): The primary set of base words used, in order, for encoding.
|
|
503
|
+
alternate_words (list[str]): The alternate (aka synonymous) set of words used, in order, for encoding.
|
|
504
|
+
|
|
505
|
+
Returns:
|
|
506
|
+
bytes: The byte representation of the DNA sequence.
|
|
507
|
+
list[int]: a mask for alternate words. 0 for word, 1 for alternate word, -1 for don't care.
|
|
508
|
+
"""
|
|
509
|
+
if len(words) != len(alternate_words):
|
|
510
|
+
raise ValueError("words and alternate_words must be the same length")
|
|
511
|
+
if len(words) < 2:
|
|
512
|
+
raise ValueError("words and alternate_words must be at least 2 long")
|
|
513
|
+
if len(dna_seq) % len(words[0]) != 0:
|
|
514
|
+
raise ValueError("DNA sequence length must be a multiple of the word length")
|
|
515
|
+
|
|
516
|
+
base = len(words)
|
|
517
|
+
word_len = len(words[0])
|
|
518
|
+
|
|
519
|
+
#create a mask for the DNA sequence
|
|
520
|
+
mask = [0]*(len(dna_seq)//word_len)
|
|
521
|
+
for i in range(len(mask)):
|
|
522
|
+
word = dna_seq[i*word_len:(i+1)*word_len]
|
|
523
|
+
if word in words:
|
|
524
|
+
mask[i] = 0
|
|
525
|
+
elif word in alternate_words:
|
|
526
|
+
mask[i] = 1
|
|
527
|
+
else:
|
|
528
|
+
raise ValueError("DNA sequence contains invalid word: {}".format(word))
|
|
529
|
+
|
|
530
|
+
#convert the DNA sequence to base N
|
|
531
|
+
bN_seq = dna_to_bN([dna_seq], words, alternate_words)[0]
|
|
532
|
+
int_value = _baseN_to_int(bN_seq, base)
|
|
533
|
+
num_words = len(dna_seq)/len(words[0])
|
|
534
|
+
num_bits = num_words * np.log2(base)
|
|
535
|
+
if __is_pow_two(base):
|
|
536
|
+
num_bits = int(np.round(num_bits)) #just incase log2 returns with a rounding error.
|
|
537
|
+
else:
|
|
538
|
+
num_bits = int(np.ceil(num_bits))
|
|
539
|
+
num_bytes = (num_bits + 7) // 8 #only works if num_bits is an int
|
|
540
|
+
|
|
541
|
+
return int_value.to_bytes(num_bytes, 'little'), mask
|
|
542
|
+
|
|
543
|
+
def chunk(b:bytes|bytearray,size:int):
|
|
544
|
+
"""break into list of size sized chunks"""
|
|
545
|
+
chunks = []
|
|
546
|
+
for i in range(0, len(b), size):
|
|
547
|
+
chunks.append( b[i:i + size] )
|
|
548
|
+
return chunks
|
|
549
|
+
|
|
550
|
+
def insert_bytes(data:bytes, insert:bytes, chunk_len:int, num_inserts:int) -> bytes:
|
|
551
|
+
"""Insert bytes into a byte array num_inserts times at chunk_len intervals. i.e.
|
|
552
|
+
the returned bytes will have the insert bytes every chunk_len bytes.
|
|
553
|
+
|
|
554
|
+
Args:
|
|
555
|
+
data (bytes): The original byte array.
|
|
556
|
+
insert (bytes): The bytes to be inserted.
|
|
557
|
+
chunk_len (int): The length of each chunk.
|
|
558
|
+
num_inserts (int): The number of insertions to be made.
|
|
559
|
+
|
|
560
|
+
Returns:
|
|
561
|
+
bytes: A list of byte chunks with the inserted bytes.
|
|
562
|
+
|
|
563
|
+
See also:
|
|
564
|
+
remove_bytes: The inverse of this function.
|
|
565
|
+
"""
|
|
566
|
+
data = bytearray(data)
|
|
567
|
+
for i in range(num_inserts):
|
|
568
|
+
ins_index = i * chunk_len
|
|
569
|
+
data[ins_index:ins_index] = insert
|
|
570
|
+
return bytes(data)
|
|
571
|
+
|
|
572
|
+
def pad_data(data:bytes,block_size:int,padding_data:bytes|str = "random") -> bytes:
|
|
573
|
+
"""Pad data to a multiple of block_size. The padding is added to the end of the data.
|
|
574
|
+
|
|
575
|
+
Args:
|
|
576
|
+
data (bytes): bytes object to be padded.
|
|
577
|
+
block_size (int): The block size to pad to. (return bytes length % block_size == 0)
|
|
578
|
+
padding_data (bytes|str, optional): The padding data to be added. Defaults to "random".
|
|
579
|
+
Returns:
|
|
580
|
+
bytes: The padded bytes.
|
|
581
|
+
"""
|
|
582
|
+
padding_size = block_size - len(data) % block_size
|
|
583
|
+
if isinstance(padding_data,bytes):
|
|
584
|
+
if len(padding_data) < padding_size:
|
|
585
|
+
raise ValueError("padding data is too small")
|
|
586
|
+
if isinstance(padding_data,str):
|
|
587
|
+
if padding_data == "random":
|
|
588
|
+
padding_data = np.random.bytes(padding_size)
|
|
589
|
+
else:
|
|
590
|
+
raise ValueError("padding data must be bytes or 'random'")
|
|
591
|
+
return data + padding_data[:padding_size]
|
|
592
|
+
|
|
593
|
+
def remove_bytes(data:bytes, insert:int|bytes, chunk_len:int, num_inserts:int) -> bytes:
|
|
594
|
+
"""Remove bytes from a byte array at specified intervals. This is the inverse of insert_bytes.
|
|
595
|
+
|
|
596
|
+
Args:
|
|
597
|
+
data (bytes): The original byte array.
|
|
598
|
+
insert_len (int|bytes): The length of the inserted bytes to be removed. If insert_len
|
|
599
|
+
is bytes, its length will be used, and the contents of the deleted bytes will be checked.
|
|
600
|
+
num_inserts (int): The number of insertions to be removed.
|
|
601
|
+
|
|
602
|
+
Returns:
|
|
603
|
+
bytes: The modified byte array with the inserted bytes removed.
|
|
604
|
+
"""
|
|
605
|
+
if isinstance(insert,bytes):
|
|
606
|
+
insert_len = len(insert)
|
|
607
|
+
else:
|
|
608
|
+
insert = None
|
|
609
|
+
|
|
610
|
+
data = bytearray(data)
|
|
611
|
+
for i in reversed(range(num_inserts)):
|
|
612
|
+
ins_index = i * chunk_len
|
|
613
|
+
if insert is not None:
|
|
614
|
+
if data[ins_index:ins_index + insert_len] != insert:
|
|
615
|
+
raise ValueError("insert bytes do not match bytes to be removed")
|
|
616
|
+
del data[ins_index:ins_index + insert_len]
|
|
617
|
+
return bytes(data)
|
|
618
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: syncoder
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: A codec for DNA data storage
|
|
5
|
+
Project-URL: Homepage, https://example.com/TBD
|
|
6
|
+
Project-URL: Issues, https://example.com/TBD/issues
|
|
7
|
+
Author-email: Christopher Takahashi <cnt@uw.edu>
|
|
8
|
+
License-Expression: BSD-3-Clause
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
Readme coming soon.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
syncoder/__init__.py,sha256=j3v6t01ai5DPLKsNpSD9_RrNMq0D4QmyICIFtIzMQmM,24
|
|
2
|
+
syncoder/codec.py,sha256=hS6n9Hdt0kBA5C9BKBFYQp3Bdt5nQttfiJp76OlUJ1A,26123
|
|
3
|
+
syncoder-0.0.0.dist-info/METADATA,sha256=wC25HovJF82XHVuDFUlqnq1sF-OcpoC0QxQ42E36xHU,376
|
|
4
|
+
syncoder-0.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
5
|
+
syncoder-0.0.0.dist-info/licenses/LICENSE,sha256=689RqJOVQ5NnDfe6w1M848LnuYiVsqIUChELJIQF0iQ,1477
|
|
6
|
+
syncoder-0.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Copyright 2025 Chris Takahashi
|
|
2
|
+
|
|
3
|
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
|
4
|
+
|
|
5
|
+
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
|
6
|
+
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
|
7
|
+
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
|
8
|
+
|
|
9
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|