syncoder 0.0.5a0__tar.gz → 0.0.6a0__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.5a0 → syncoder-0.0.6a0}/PKG-INFO +7 -6
- {syncoder-0.0.5a0 → syncoder-0.0.6a0}/README.md +29 -27
- {syncoder-0.0.5a0 → syncoder-0.0.6a0}/environments/conda_environment.yml +3 -3
- {syncoder-0.0.5a0 → syncoder-0.0.6a0}/environments/conda_environment_dev.yml +3 -3
- syncoder-0.0.6a0/environments/conda_environment_pipeline.yml +14 -0
- {syncoder-0.0.5a0 → syncoder-0.0.6a0}/environments/conda_environment_test.yml +3 -3
- {syncoder-0.0.5a0 → syncoder-0.0.6a0}/pyproject.toml +33 -34
- {syncoder-0.0.5a0 → syncoder-0.0.6a0}/src/syncoder/__init__.py +6 -6
- {syncoder-0.0.5a0 → syncoder-0.0.6a0}/src/syncoder/codec.py +248 -58
- syncoder-0.0.6a0/src/syncoder/sector01.py +57 -0
- {syncoder-0.0.5a0 → syncoder-0.0.6a0}/tests/test_codec_pytest.py +26 -2
- {syncoder-0.0.5a0 → syncoder-0.0.6a0}/.gitignore +0 -0
- {syncoder-0.0.5a0 → syncoder-0.0.6a0}/.vscode/.gitignore +0 -0
- {syncoder-0.0.5a0 → syncoder-0.0.6a0}/LICENSE +0 -0
- {syncoder-0.0.5a0 → syncoder-0.0.6a0}/src/syncoder/reedsoloMulti.py +0 -0
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: syncoder
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.6a0
|
|
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
|
|
7
7
|
Author-email: Christopher Takahashi <cnt@uw.edu>
|
|
8
8
|
License-Expression: BSD-3-Clause
|
|
9
9
|
License-File: LICENSE
|
|
10
|
-
Requires-Python: >=3.
|
|
11
|
-
Requires-Dist: galois>=0.4
|
|
12
|
-
Requires-Dist: numpy>=
|
|
13
|
-
Requires-Dist: reedsolo>=1.7
|
|
10
|
+
Requires-Python: >=3.11
|
|
11
|
+
Requires-Dist: galois>=0.4.8
|
|
12
|
+
Requires-Dist: numpy>=2
|
|
14
13
|
Description-Content-Type: text/markdown
|
|
15
14
|
|
|
16
15
|
# SyNCODER
|
|
@@ -20,6 +19,8 @@ A DNA codec. (Fill in.)
|
|
|
20
19
|
|
|
21
20
|
## Features
|
|
22
21
|
Fill in here.
|
|
22
|
+
### DDSA Sector 0/1 generator
|
|
23
|
+
(Status: incomplete) Generates [sector 0](https://www.snia.org/standards/technology-standards-software/standards-portfolio/dna-data-storage-sector-zero) and [sector 1](https://www.snia.org/standards/technology-standards-software/standards-portfolio/dna-data-storage-sector-one) compliant with the DNA Data Storange Alliance (DDSA) [DNA Archive Rosetta Stone specifiation](https://dnastoragealliance.org/publications/). Note: The maximum sector 1 size is currenty 325950 bytes (limited by reserved index size).
|
|
23
24
|
|
|
24
25
|
## Installation
|
|
25
26
|
`codec.py` Can be included in a project and used as a local module or installed and used from `pip` :
|
|
@@ -1,27 +1,29 @@
|
|
|
1
|
-
# SyNCODER
|
|
2
|
-
|
|
3
|
-
## Project Overview
|
|
4
|
-
A DNA codec. (Fill in.)
|
|
5
|
-
|
|
6
|
-
## Features
|
|
7
|
-
Fill in here.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
1
|
+
# SyNCODER
|
|
2
|
+
|
|
3
|
+
## Project Overview
|
|
4
|
+
A DNA codec. (Fill in.)
|
|
5
|
+
|
|
6
|
+
## Features
|
|
7
|
+
Fill in here.
|
|
8
|
+
### DDSA Sector 0/1 generator
|
|
9
|
+
(Status: incomplete) Generates [sector 0](https://www.snia.org/standards/technology-standards-software/standards-portfolio/dna-data-storage-sector-zero) and [sector 1](https://www.snia.org/standards/technology-standards-software/standards-portfolio/dna-data-storage-sector-one) compliant with the DNA Data Storange Alliance (DDSA) [DNA Archive Rosetta Stone specifiation](https://dnastoragealliance.org/publications/). Note: The maximum sector 1 size is currenty 325950 bytes (limited by reserved index size).
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
`codec.py` Can be included in a project and used as a local module or installed and used from `pip` :
|
|
13
|
+
```
|
|
14
|
+
pip install --pre syncoder
|
|
15
|
+
```
|
|
16
|
+
Note the requirement for the `--pre` flag as there is no official post release yet.
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
To be completed. For now see `tests/test_codec_pytest.py` for examples
|
|
20
|
+
|
|
21
|
+
## Testing
|
|
22
|
+
It is sufficient to run
|
|
23
|
+
```
|
|
24
|
+
pytest
|
|
25
|
+
```
|
|
26
|
+
from the root directory. Currently coverage is only for features and parameters in active use. More tests to come.
|
|
27
|
+
|
|
28
|
+
## License
|
|
29
|
+
BSD-3 clause. see `LICENSE` file in this directory.
|
|
@@ -5,9 +5,9 @@ dependencies:
|
|
|
5
5
|
- python>=3.13
|
|
6
6
|
- pip
|
|
7
7
|
- "libblas=*=*mkl"
|
|
8
|
-
- numpy
|
|
8
|
+
- numpy
|
|
9
9
|
- matplotlib=3.*
|
|
10
|
-
- numba=0.
|
|
10
|
+
- numba=0.62.* # version pinned for galois 0.4.8
|
|
11
11
|
- pytest
|
|
12
12
|
- sphinx=7.*
|
|
13
13
|
- sphinx_rtd_theme
|
|
@@ -16,6 +16,6 @@ dependencies:
|
|
|
16
16
|
- ipykernel
|
|
17
17
|
- twine
|
|
18
18
|
- pip:
|
|
19
|
-
- galois>=0.4.
|
|
19
|
+
- galois>=0.4.8
|
|
20
20
|
- build
|
|
21
21
|
|
|
@@ -4,12 +4,12 @@ channels:
|
|
|
4
4
|
dependencies:
|
|
5
5
|
- python>=3.13
|
|
6
6
|
- pip
|
|
7
|
-
- numpy=2.2.6
|
|
8
|
-
- numba=0.
|
|
7
|
+
- numpy=2.2.6
|
|
8
|
+
- numba=0.62.* # version pinned for galois 0.4.8
|
|
9
9
|
- pytest
|
|
10
10
|
- sphinx=7.*
|
|
11
11
|
- sphinx_rtd_theme
|
|
12
12
|
- twine
|
|
13
13
|
- pip:
|
|
14
|
-
- galois>=0.4.
|
|
14
|
+
- galois>=0.4.8
|
|
15
15
|
- build
|
|
@@ -1,35 +1,34 @@
|
|
|
1
|
-
[project]
|
|
2
|
-
name = "syncoder"
|
|
3
|
-
dynamic = ["version"]
|
|
4
|
-
dependencies = [
|
|
5
|
-
"numpy>=
|
|
6
|
-
"galois>=0.4"
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
license = "
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
[
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
[
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
[
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
[tool.hatch.version]
|
|
1
|
+
[project]
|
|
2
|
+
name = "syncoder"
|
|
3
|
+
dynamic = ["version"]
|
|
4
|
+
dependencies = [
|
|
5
|
+
"numpy>=2",
|
|
6
|
+
"galois>=0.4.8"
|
|
7
|
+
]
|
|
8
|
+
description = "A codec for DNA data storage"
|
|
9
|
+
authors = [
|
|
10
|
+
{ name="Christopher Takahashi", email="cnt@uw.edu" }
|
|
11
|
+
]
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.11"
|
|
14
|
+
license = "BSD-3-Clause"
|
|
15
|
+
license-files = ["LICENSE"]
|
|
16
|
+
|
|
17
|
+
[project.urls]
|
|
18
|
+
Homepage = "https://example.com/TBD"
|
|
19
|
+
Issues = "https://example.com/TBD/issues"
|
|
20
|
+
|
|
21
|
+
[tool.setuptools]
|
|
22
|
+
packages = ["syncoder"]
|
|
23
|
+
|
|
24
|
+
[build-system]
|
|
25
|
+
requires = ["hatchling >= 1.26"]
|
|
26
|
+
build-backend = "hatchling.build"
|
|
27
|
+
|
|
28
|
+
[tool.pytest.ini_options]
|
|
29
|
+
testpaths = [
|
|
30
|
+
"tests"
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[tool.hatch.version]
|
|
35
34
|
path = "src/syncoder/__init__.py"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
from .codec import *
|
|
2
|
-
from .codec import _default_b32_alphabet, _default_b32_alphabet_alt
|
|
3
|
-
default_b32_alphabet = _default_b32_alphabet
|
|
4
|
-
default_b32_alphabet_alt = _default_b32_alphabet_alt
|
|
5
|
-
|
|
6
|
-
__version__ = "0.0.
|
|
1
|
+
from .codec import *
|
|
2
|
+
from .codec import _default_b32_alphabet, _default_b32_alphabet_alt
|
|
3
|
+
default_b32_alphabet = _default_b32_alphabet
|
|
4
|
+
default_b32_alphabet_alt = _default_b32_alphabet_alt
|
|
5
|
+
|
|
6
|
+
__version__ = "0.0.6a0"
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import struct
|
|
3
|
-
import itertools
|
|
4
1
|
import logging
|
|
5
2
|
import typing
|
|
3
|
+
import json
|
|
4
|
+
from . import sector01
|
|
6
5
|
from typing import Optional,Union, Callable, Tuple, Literal, cast #requires python 3.8 or later
|
|
7
6
|
|
|
8
7
|
|
|
@@ -11,17 +10,7 @@ from numpy.typing import ArrayLike
|
|
|
11
10
|
|
|
12
11
|
import galois
|
|
13
12
|
|
|
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
|
|
22
|
-
|
|
23
13
|
#### ROADMAP ####
|
|
24
|
-
# - integrate the baseN to DNA function here. (generalize from b32fn)
|
|
25
14
|
# - Strand prefix sub-encoder.
|
|
26
15
|
# - Sample objective functions (at least minimize homopolymers and avoid annealing.)
|
|
27
16
|
|
|
@@ -138,15 +127,14 @@ class BaseNBlockCodec:
|
|
|
138
127
|
n_strands (int): Number of "strands" stored by this codec including redundant strands.
|
|
139
128
|
max_strand_index (Optional[int]): Maximum guaranteed index. Default: n_strands.
|
|
140
129
|
index_type (str): Type of index to use. Options: "binary", "inner symbol", "inner". Default: "binary".
|
|
130
|
+
index_type "binary" integrates the index into the data stream as a binary number.
|
|
131
|
+
This may help packing efficiency.
|
|
132
|
+
index_type "inner symbol"/"inner" integrates the index inner code symbol symbols.
|
|
133
|
+
This may help packing efficiency and always simplifies index extraction.
|
|
141
134
|
index_location (str): Location of the index within the inner code. Options: "middle", "beginning", "end". Default: "middle".
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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)
|
|
135
|
+
index_location "middle" (default) places the index after the data symbols but before the redundancy symbols.
|
|
136
|
+
index_location "beginning" places the index before the data symbols.
|
|
137
|
+
index_location "end" places the index after the redundancy symbols. (not implemented)
|
|
150
138
|
"""
|
|
151
139
|
if n_strands<=255:
|
|
152
140
|
self.outer_alphabet_size_bytes = 1
|
|
@@ -159,6 +147,7 @@ class BaseNBlockCodec:
|
|
|
159
147
|
max_strand_index = n_strands
|
|
160
148
|
if max_strand_index<n_strands:
|
|
161
149
|
raise ValueError("max_strand_index must be greater than or equal to n_strands.")
|
|
150
|
+
self.max_strand_index = max_strand_index
|
|
162
151
|
|
|
163
152
|
if index_type not in {"binary","inner symbol","inner"}:
|
|
164
153
|
raise ValueError("index_type must be 'binary' or 'inner symbol'.")
|
|
@@ -183,6 +172,7 @@ class BaseNBlockCodec:
|
|
|
183
172
|
# make sure extra sequence numbers are not added to inner blocks. and or maybe add a
|
|
184
173
|
# parameter for the number of inner blocks per strand.
|
|
185
174
|
assert inner_n<inner_alphabet_size
|
|
175
|
+
self.inner_n = inner_n
|
|
186
176
|
|
|
187
177
|
self.inner_coder = galois.ReedSolomon(n=inner_alphabet_size-1,d=inner_d,field=galois.GF(inner_alphabet_size))
|
|
188
178
|
# in units of symbols (there are q=inner_alphabet_size possible symbols)
|
|
@@ -199,12 +189,12 @@ class BaseNBlockCodec:
|
|
|
199
189
|
self.inner_k = k
|
|
200
190
|
q = self.inner_coder.field.order
|
|
201
191
|
|
|
202
|
-
#number of bytes needed to store the index
|
|
203
|
-
self.index_bits = np.ceil(np.log2(max_strand_index)).astype(int)
|
|
204
|
-
logging.debug("bits per strand used for indexing: {}".format(self.index_bits))
|
|
205
192
|
|
|
206
193
|
#message length in bytes (without index)
|
|
207
194
|
if self.index_type == "binary":
|
|
195
|
+
#number of bytes needed to store the index
|
|
196
|
+
self.index_bits = np.ceil(np.log2(max_strand_index)).astype(int)
|
|
197
|
+
logging.debug("bits per strand used for indexing: {}".format(self.index_bits))
|
|
208
198
|
self.data_chunk_size = int(k*np.log2(q)-self.index_bits) // 8
|
|
209
199
|
elif self.index_type == "inner":
|
|
210
200
|
self.index_symbols = len(_int_to_baseN(max_strand_index,q))
|
|
@@ -222,7 +212,14 @@ class BaseNBlockCodec:
|
|
|
222
212
|
logging.debug( "codec message size: {} bytes".format(self.block_capacity_bytes) )
|
|
223
213
|
|
|
224
214
|
#init the outer coder
|
|
225
|
-
self.
|
|
215
|
+
if self.outer_alphabet_size_bytes==2 and self.data_chunk_size%2!=0:
|
|
216
|
+
raise ValueError("data_chunk_size must be even for 16 bit outer code.")
|
|
217
|
+
#c=0 to match reedsolo library's default for code compatibility with prior versions of this codec.
|
|
218
|
+
# This may be changed in a future major version.
|
|
219
|
+
self.outer_coder_fast = galois.ReedSolomon(n=2**(self.outer_alphabet_size_bytes*8)-1,
|
|
220
|
+
d=n_redundant_strands+1,
|
|
221
|
+
c=0,
|
|
222
|
+
field=galois.GF(2**(self.outer_alphabet_size_bytes*8)))
|
|
226
223
|
|
|
227
224
|
def __compute_waste(self):
|
|
228
225
|
k = self.inner_k
|
|
@@ -243,6 +240,77 @@ class BaseNBlockCodec:
|
|
|
243
240
|
else:
|
|
244
241
|
logging.debug("{} symbols wasted per strand".format( _wasted_symbols))
|
|
245
242
|
|
|
243
|
+
def generate_sector1(self, alphabets,vendor_id = "UNKNOWN", primer_len=40, file_meta_data=None):
|
|
244
|
+
#change to lazy imports when python 3.15 is common.
|
|
245
|
+
#these don't take particularly long to import but i don't want to bloat things more.
|
|
246
|
+
import sys
|
|
247
|
+
import uuid
|
|
248
|
+
import json
|
|
249
|
+
from collections import OrderedDict
|
|
250
|
+
|
|
251
|
+
alen = len(alphabets[0])
|
|
252
|
+
abases = len(alphabets[0][0])
|
|
253
|
+
|
|
254
|
+
for a in alphabets:
|
|
255
|
+
if len(a) != alen:
|
|
256
|
+
raise ValueError("All alphabets must be of the same length")
|
|
257
|
+
for s in a:
|
|
258
|
+
if len(s) != abases:
|
|
259
|
+
raise ValueError("All elements of alphabets must be the same number of bases")
|
|
260
|
+
|
|
261
|
+
files_nstrands = 0
|
|
262
|
+
if file_meta_data is not None:
|
|
263
|
+
for block_name, block_meta in file_meta_data.items():
|
|
264
|
+
strand_count_code_block = block_meta["blen"]//self.data_chunk_size+self.outer_coder_fast.d-1
|
|
265
|
+
strand_count = strand_count_code_block*len(block_meta["istart"])
|
|
266
|
+
files_nstrands+=strand_count
|
|
267
|
+
|
|
268
|
+
#encode as one string to save space.
|
|
269
|
+
alphabets = [" ".join(x) for x in alphabets]
|
|
270
|
+
|
|
271
|
+
sector1 = OrderedDict(sector01.get_sector_1_obj())
|
|
272
|
+
#reserve octets 4-5 for the number of strands in sector 1.
|
|
273
|
+
u = bytearray(uuid.uuid4().bytes)
|
|
274
|
+
u[6] = 8<<4
|
|
275
|
+
u[8] = (u[8] & 0b00111111) | 0b10000000
|
|
276
|
+
u[4] = 0
|
|
277
|
+
u[5] = 0
|
|
278
|
+
sector1["id"] = str(uuid.UUID(bytes=bytes(u) ))
|
|
279
|
+
sector1["codec"]["ver"] = sys.modules['.'.join(__name__.split('.')[:-1])].__version__
|
|
280
|
+
codec_params = sector1["codec"]["params"]
|
|
281
|
+
codec_params["alphs"] = alphabets
|
|
282
|
+
codec_params["innerd"] = self.inner_coder.d
|
|
283
|
+
codec_params["innern"] = self.inner_n
|
|
284
|
+
codec_params["nstrands"] = self.n_strands
|
|
285
|
+
codec_params["neccstrand"] = self.outer_coder_fast.d-1
|
|
286
|
+
codec_params["imax"] = self.max_strand_index
|
|
287
|
+
codec_params["itype"] = self.index_type
|
|
288
|
+
codec_params["ilocation"] = self.index_location
|
|
289
|
+
sector1["vendorid"] = vendor_id
|
|
290
|
+
seq_params = sector1["seq"]
|
|
291
|
+
osize = self.inner_n*abases+primer_len
|
|
292
|
+
seq_params["osizemin"] = osize
|
|
293
|
+
seq_params["osizemedian"] = osize
|
|
294
|
+
seq_params["osizemax"] = osize
|
|
295
|
+
seq_params["ocount"] = files_nstrands
|
|
296
|
+
#these two don't make sense
|
|
297
|
+
seq_params["orepmin"] = 1
|
|
298
|
+
seq_params["orepmax"] = 100
|
|
299
|
+
|
|
300
|
+
if file_meta_data is not None:
|
|
301
|
+
sector1["addl"] = {"fmeta": file_meta_data}
|
|
302
|
+
|
|
303
|
+
sector1.move_to_end("id",last=False) #guarentee this is in the first code block.
|
|
304
|
+
#add this in when file meta is present.
|
|
305
|
+
#s1_len = len(json.dumps(sector1,separators=(',', ':')).encode('utf-8'))
|
|
306
|
+
#s1_strands = s1_len//self.data_chunk_size + (1 if s1_len%self.data_chunk_size>0 else 0)
|
|
307
|
+
#s1_redundant_strands = 50*(s1_len//self.block_capacity_bytes + (1 if s1_len%self.block_capacity_bytes>0 else 0))
|
|
308
|
+
#s1_strands += s1_redundant_strands
|
|
309
|
+
|
|
310
|
+
#sector1["id"] = uid+f":{s1_strands:05d}"
|
|
311
|
+
|
|
312
|
+
return sector1
|
|
313
|
+
|
|
246
314
|
def encode(self,data:bytes,index_start:int=0)->list[list[int]]:
|
|
247
315
|
""" Encodes the given data using the outer and inner codes.
|
|
248
316
|
|
|
@@ -293,11 +361,11 @@ class BaseNBlockCodec:
|
|
|
293
361
|
#1. Reshape data
|
|
294
362
|
logging.debug("applying outer code to {} bytes".format(len(data)))
|
|
295
363
|
# reshape data so columns can be coded independently.
|
|
296
|
-
if self.
|
|
364
|
+
if self.outer_alphabet_size_bytes==1:
|
|
297
365
|
data_np = np.frombuffer(data,dtype=np.uint8)
|
|
298
366
|
assert len(data_np)%self.data_chunk_size == 0
|
|
299
367
|
data_np = data_np.reshape(-1,self.data_chunk_size)
|
|
300
|
-
elif self.
|
|
368
|
+
elif self.outer_alphabet_size_bytes==2:
|
|
301
369
|
data_np = np.frombuffer(data,dtype=np.uint16)
|
|
302
370
|
if self.data_chunk_size%2!=0:
|
|
303
371
|
raise ValueError("data_chunk_size must be even for 16 bit outer code.")
|
|
@@ -309,7 +377,8 @@ class BaseNBlockCodec:
|
|
|
309
377
|
raise ValueError("Unsupported outer code byte size.")
|
|
310
378
|
|
|
311
379
|
#2. apply the outer code (encode columns of data)
|
|
312
|
-
data_enc_cols = np.array([self.
|
|
380
|
+
data_enc_cols = np.array([self.outer_coder_fast.encode(dc) for dc in data_np.transpose()],dtype=data_np.dtype)
|
|
381
|
+
|
|
313
382
|
data_enc_chunks = data_enc_cols.transpose()
|
|
314
383
|
|
|
315
384
|
logging.debug("outer coded strands: {}".format(data_enc_chunks.shape[0]))
|
|
@@ -347,16 +416,17 @@ class BaseNBlockCodec:
|
|
|
347
416
|
|
|
348
417
|
return ic_chunks
|
|
349
418
|
|
|
350
|
-
def decode(self, data: list[list[int]], index_start: int = 0) -> Tuple[bytes,ArrayLike,ArrayLike,ArrayLike]:
|
|
419
|
+
def decode(self, data: list[list[int]], index_start: int = 0, n_strands: int|None = None) -> Tuple[bytes|None,ArrayLike,ArrayLike,ArrayLike]:
|
|
351
420
|
"""
|
|
352
421
|
Decodes the given data.
|
|
353
422
|
|
|
354
423
|
Args:
|
|
355
424
|
data (list[list[int]]): The encoded data to be decoded.
|
|
356
425
|
index_start (int, optional): The starting index for decoding. Defaults to 0.
|
|
426
|
+
n_strands (int, optional): The number of strands expected. Defaults to n_strands set in constructor.
|
|
357
427
|
|
|
358
428
|
Returns:
|
|
359
|
-
bytes: The decoded data.
|
|
429
|
+
bytes|None: The decoded data. Or None if decoding failed.
|
|
360
430
|
outer erasures: list of erasures in outer code.
|
|
361
431
|
outer errors: list of errors in outer code.
|
|
362
432
|
inner errors: list of number of errors in inner code by strand index. -1 -> strand not present.
|
|
@@ -364,17 +434,23 @@ class BaseNBlockCodec:
|
|
|
364
434
|
Raises:
|
|
365
435
|
None
|
|
366
436
|
"""
|
|
437
|
+
if n_strands is None:
|
|
438
|
+
n_strands = self.n_strands
|
|
439
|
+
|
|
367
440
|
# inner decode
|
|
368
441
|
## TODO: handle erasures in inner code.
|
|
369
442
|
|
|
370
443
|
# I'm sure it'll be a (FieldArray,int) ----------------V
|
|
371
|
-
chunked_data:list[Tuple[galois.FieldArray,int]] = [self.inner_coder.decode(d,errors=True) for d in data] #type:ignore
|
|
444
|
+
chunked_data:list[Tuple[galois.FieldArray,int]] = [self.inner_coder.decode(d,errors=True) for d in data if d != []] #type:ignore
|
|
372
445
|
|
|
373
446
|
|
|
374
447
|
# handle outer decode
|
|
375
|
-
ordered_chunks:list[bytes|None] = [None] *
|
|
376
|
-
chunk_errors:list[int] = [-1] *
|
|
448
|
+
ordered_chunks:list[bytes|None] = [None] * n_strands
|
|
449
|
+
chunk_errors:list[int] = [-1] * n_strands
|
|
377
450
|
for chunk in chunked_data:
|
|
451
|
+
#TODO: decide if its better to throw these out or keep them.
|
|
452
|
+
#if chunk[1]<0: #can't decode and the address may be bad so discard.
|
|
453
|
+
# continue
|
|
378
454
|
if self.index_type == "binary":
|
|
379
455
|
chunk_int = _baseN_to_int(chunk[0].tolist(), self.inner_coder.field.order)
|
|
380
456
|
chunk_index = (chunk_int >> (self.data_chunk_size * 8)) - index_start
|
|
@@ -391,12 +467,16 @@ class BaseNBlockCodec:
|
|
|
391
467
|
raise NotImplementedError("index_location 'end' is not implemented.")
|
|
392
468
|
else:
|
|
393
469
|
raise ValueError("Unknown index type: {}".format(self.index_type)) #should be impossible to get here.
|
|
394
|
-
|
|
470
|
+
#this can be to big sometimes. TODO handle more gracefully
|
|
395
471
|
try:
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
472
|
+
chunk_bytes:bytes = chunk_int.to_bytes(self.data_chunk_size, 'little')
|
|
473
|
+
try:
|
|
474
|
+
ordered_chunks[chunk_index] = chunk_bytes
|
|
475
|
+
chunk_errors[chunk_index] = chunk[1]
|
|
476
|
+
except IndexError:
|
|
477
|
+
logging.info("strand index out of bounds:" + str(chunk_index))
|
|
478
|
+
except OverflowError:
|
|
479
|
+
logging.info("chunk overflow for strand index:" + str(chunk_index)+ "intval: "+ str(chunk_int))
|
|
400
480
|
|
|
401
481
|
# identify erasures
|
|
402
482
|
erasures = []
|
|
@@ -407,20 +487,32 @@ class BaseNBlockCodec:
|
|
|
407
487
|
#we're all bytes now, but the checker can't tell that.
|
|
408
488
|
ordered_chunks_bytes:list[bytes] = typing.cast(list[bytes],ordered_chunks)
|
|
409
489
|
|
|
410
|
-
|
|
490
|
+
# report erasures (lost strands) in outer code
|
|
491
|
+
logging.info("erasures (lost) chunks: {}".format(erasures))
|
|
492
|
+
|
|
493
|
+
if self.outer_alphabet_size_bytes==1:
|
|
411
494
|
outer_dtype = np.uint8
|
|
412
|
-
elif self.
|
|
495
|
+
elif self.outer_alphabet_size_bytes==2:
|
|
413
496
|
outer_dtype = np.uint16
|
|
414
497
|
else:
|
|
415
498
|
raise ValueError("Unsupported outer code byte size.")
|
|
416
|
-
data_np_encoded = np.array([np.frombuffer(c, dtype=outer_dtype) for c in ordered_chunks_bytes]).transpose()
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
499
|
+
data_np_encoded = np.array([np.frombuffer(c, dtype=outer_dtype) for c in ordered_chunks_bytes]).transpose()
|
|
500
|
+
|
|
501
|
+
has_erasuers = len(erasures)>0
|
|
502
|
+
erasures_bool = np.zeros(data_np_encoded.shape[1],dtype=bool)
|
|
503
|
+
if has_erasuers:
|
|
504
|
+
erasures_bool[erasures] = True
|
|
505
|
+
data_decoded_results = [self.outer_coder_fast.decode(dc,errors=True,erasures=erasures_bool) for dc in data_np_encoded]
|
|
506
|
+
data_errata_col_pos = [] #galois doesn't report error positions
|
|
507
|
+
data_num_errors = [int(x[1]) for x in data_decoded_results]
|
|
508
|
+
if -1 in data_num_errors:
|
|
509
|
+
logging.error("Outer code decode failed on at least one column.")
|
|
510
|
+
return None, erasures, data_num_errors, chunk_errors
|
|
511
|
+
#raise ValueError(f"Fast mode outer code decode failed on at least one column. numerrors:{data_num_errors}")
|
|
512
|
+
logging.info("Found {} outer code column errors.".format(data_num_errors))
|
|
513
|
+
data_decoded_chunks = np.array([np.array(d[0]) for d in data_decoded_results],dtype=outer_dtype).transpose()
|
|
420
514
|
|
|
421
|
-
|
|
422
|
-
logging.info("erasures (lost) chunks: {}".format(erasures))
|
|
423
|
-
data_errata_col_pos = [e[2] for e in data_decoded_results]
|
|
515
|
+
data_decoded = data_decoded_chunks.flatten().tobytes()
|
|
424
516
|
|
|
425
517
|
errors = []
|
|
426
518
|
for i, pos in enumerate(data_errata_col_pos):
|
|
@@ -434,20 +526,23 @@ class BaseNBlockCodec:
|
|
|
434
526
|
if e[0] in erasures:
|
|
435
527
|
pass
|
|
436
528
|
else:
|
|
437
|
-
logging.
|
|
529
|
+
logging.info("Outer code errors found on strand {}, byte {}.".format(*e))
|
|
530
|
+
|
|
531
|
+
return data_decoded, erasures, data_num_errors, chunk_errors # pyright: ignore[reportPossiblyUnboundVariable]
|
|
438
532
|
|
|
439
|
-
return data_decoded, erasures, errors, chunk_errors
|
|
440
533
|
|
|
441
|
-
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:
|
|
534
|
+
def extract_index_dna(self, dna: str|bytes, words:list[str], alternate_words:list[str], error_check: bool|Literal["erasures"] = False, padding: Literal["left","right"]="right") -> int:
|
|
442
535
|
"""
|
|
443
536
|
Extracts the index from the given DNA strand.
|
|
444
537
|
|
|
445
538
|
Args:
|
|
446
539
|
dna (str|bytes): The input DNA strand from which to extract the index.
|
|
447
|
-
error_check (bool, optional): If True, the inner_codec will be used to
|
|
448
|
-
attempt to check for errors.
|
|
449
|
-
|
|
450
|
-
|
|
540
|
+
error_check (bool|Literal["erasures"], optional): If True, the inner_codec will be used to
|
|
541
|
+
attempt to check for errors. if "erasures", the inner codec will check only if it encounters an N.
|
|
542
|
+
Defaults to False.
|
|
543
|
+
padding Literal["left","right"], optional): Assume any missing bases are left or right of index.
|
|
544
|
+
Defaults to "right".
|
|
545
|
+
Note: padded sequences are unlikely to error correct properly.
|
|
451
546
|
words (list[str]): The primary set of base words used, in order, for encoding.
|
|
452
547
|
alternate_words (list[str]): The alternate (aka synonymous) set of words used, in order, for encoding.
|
|
453
548
|
|
|
@@ -466,12 +561,30 @@ class BaseNBlockCodec:
|
|
|
466
561
|
elif padding == "left":
|
|
467
562
|
dna_s = "A"*num_missing + dna_s
|
|
468
563
|
|
|
564
|
+
|
|
565
|
+
if error_check == "erasures":
|
|
566
|
+
erasures = np.zeros(len(dna_s)//symbol_len,dtype=bool)
|
|
567
|
+
_temp = []
|
|
568
|
+
for i,c in enumerate(dna_s):
|
|
569
|
+
if c.upper() not in "ACGT":
|
|
570
|
+
erasures[i//symbol_len] = True
|
|
571
|
+
_temp.append("A")
|
|
572
|
+
else:
|
|
573
|
+
_temp.append(c)
|
|
574
|
+
has_erasures = bool(np.any(erasures))
|
|
575
|
+
if has_erasures: #don't bother if nothing has changed.
|
|
576
|
+
dna_s = "".join(_temp)
|
|
577
|
+
else:
|
|
578
|
+
has_erasures = False
|
|
579
|
+
erasures = None
|
|
580
|
+
|
|
581
|
+
ec = (has_erasures and error_check=="erasures") or (error_check==True)
|
|
469
582
|
#This could fail if padding makes a symbol not in words or alternate_words.
|
|
470
583
|
data = dna_to_bN([dna_s],words,alternate_words)[0]
|
|
471
584
|
|
|
472
|
-
return self.extract_index(data,
|
|
585
|
+
return self.extract_index(data,ec,erasures)
|
|
473
586
|
|
|
474
|
-
def extract_index(self, data: list[int], error_check: bool = False) -> int:
|
|
587
|
+
def extract_index(self, data: list[int], error_check: bool = False, erasures: np.ndarray|None = None) -> int:
|
|
475
588
|
"""
|
|
476
589
|
Extracts the index from the given inner code sequence.
|
|
477
590
|
|
|
@@ -480,6 +593,7 @@ class BaseNBlockCodec:
|
|
|
480
593
|
If a list[int] is given, it is assumed be be inner code symbols.
|
|
481
594
|
error_check (bool, optional): If True, the inner_codec will be used to
|
|
482
595
|
attempt to check for errors. Defaults to False.
|
|
596
|
+
erasures (np.ndarray|None, optional): A boolean array indicating which positions are erased. Defaults to None.
|
|
483
597
|
|
|
484
598
|
Returns:
|
|
485
599
|
int: The index. or -1 if error_check==True and unrecoverable error found.
|
|
@@ -487,7 +601,7 @@ class BaseNBlockCodec:
|
|
|
487
601
|
if error_check == False:
|
|
488
602
|
data = data[0:self.inner_k] #extract message
|
|
489
603
|
else:
|
|
490
|
-
data_gf, n_corrected = self.inner_coder.decode(data,errors=True)
|
|
604
|
+
data_gf, n_corrected = self.inner_coder.decode(data,errors=True,erasures=erasures) #type:ignore
|
|
491
605
|
data = data_gf.tolist()
|
|
492
606
|
if n_corrected < 0:
|
|
493
607
|
return -1
|
|
@@ -549,7 +663,7 @@ def longest_binder(a:str,b:str):
|
|
|
549
663
|
|
|
550
664
|
|
|
551
665
|
|
|
552
|
-
def b32_to_DNA_optimize(file_data:list[ArrayLike],words:list[str], alternate_words:list[str], mask:Union[ArrayLike,None]=None, nmasked=-1, penalty_fn:Union[Callable[[str],int],None]=None)->list[Tuple[bytes,int]]:
|
|
666
|
+
def b32_to_DNA_optimize(file_data:list[ArrayLike],words:list[str], alternate_words:list[str], mask:Union[ArrayLike,None]=None, nmasked:int =-1, penalty_fn:Union[Callable[[str],int],None]=None)->list[Tuple[bytes,int]]:
|
|
553
667
|
"""
|
|
554
668
|
TODO: rename to bN_to_DNA_optimize, and add test.
|
|
555
669
|
file_data: 2d list of integral types (symbols)
|
|
@@ -810,3 +924,79 @@ def remove_bytes(data:bytes, insert:int|bytes, chunk_len:int, num_inserts:int) -
|
|
|
810
924
|
del data[ins_index:ins_index + insert_len]
|
|
811
925
|
return bytes(data)
|
|
812
926
|
|
|
927
|
+
|
|
928
|
+
########################
|
|
929
|
+
# Sector 0 and 1 funcs #
|
|
930
|
+
########################
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
def sector_1_codec():
|
|
934
|
+
"""Returns a BaseNBlockCodec object with parameters suitable for encoding sector 1 of the SyNCODER format."""
|
|
935
|
+
return BaseNBlockCodec(inner_alphabet_size=32,
|
|
936
|
+
inner_d=5,
|
|
937
|
+
inner_n=70//3,
|
|
938
|
+
n_strands=255,
|
|
939
|
+
n_redundant_strands=50,
|
|
940
|
+
max_strand_index=2**(5*3)-1,#32,767 strands max.
|
|
941
|
+
index_type="inner")
|
|
942
|
+
|
|
943
|
+
def generate_sector1(codec:BaseNBlockCodec,alphabets:list[list[str]],vendor_id:str, file_meta_data:dict, primer_len:int=40, length:int=-1) -> list[bytes]:
|
|
944
|
+
"""Generates the sector 1 object for the SyNCODER format.
|
|
945
|
+
|
|
946
|
+
Args:
|
|
947
|
+
alphabets (list of list of str): A list of alphabets, where each alphabet is a list of strings representing the symbols in that alphabet.
|
|
948
|
+
vendor_id (str): A DNA string representing the vendor ID.
|
|
949
|
+
file_meta_data (dict): A dictionary containing metadata about the files being encoded.
|
|
950
|
+
primer_len (int, optional): The length of the primers to be used in sector 1. Defaults to 40.
|
|
951
|
+
length (int, optional): The number of strands in the sector 1 object. If -1, the number of strands will be determined automatically. Defaults to -1.
|
|
952
|
+
|
|
953
|
+
Returns:
|
|
954
|
+
list[bytes]: A list of bytes representing the encoded sector 1 object.
|
|
955
|
+
"""
|
|
956
|
+
|
|
957
|
+
assert len(vendor_id) == 35 #required by spec.
|
|
958
|
+
s1codec = sector_1_codec()
|
|
959
|
+
sector1 = codec.generate_sector1(alphabets,vendor_id,primer_len,file_meta_data)
|
|
960
|
+
|
|
961
|
+
if length>=0:
|
|
962
|
+
nstarnds_ashex = hex(length)[2:].rjust(4,'0')
|
|
963
|
+
id_parts = sector1["id"].split("-")
|
|
964
|
+
id_parts[1] = nstarnds_ashex
|
|
965
|
+
sector1["id"] = "-".join(id_parts)
|
|
966
|
+
|
|
967
|
+
s1json = json.dumps(sector1,separators=(',', ':')).encode('utf-8')
|
|
968
|
+
|
|
969
|
+
padding = s1codec.data_chunk_size - (len(s1json) % s1codec.data_chunk_size)
|
|
970
|
+
if padding != s1codec.data_chunk_size:
|
|
971
|
+
# Distribute padding bytes by inserting at most one space after commas
|
|
972
|
+
new_chars = bytearray(b" ") #put one space at the start.
|
|
973
|
+
rem = padding-1
|
|
974
|
+
for c in s1json:
|
|
975
|
+
new_chars.append(c)
|
|
976
|
+
if c == ',' and rem > 0:
|
|
977
|
+
new_chars.append(ord(' ')) # add a space after the comma
|
|
978
|
+
rem -= 1
|
|
979
|
+
if rem > 0: #append any remaining spaces at the end (probably wont happen)
|
|
980
|
+
new_chars.extend(b" " * rem)
|
|
981
|
+
s1json_padded = bytes(new_chars)
|
|
982
|
+
assert json.loads(s1json_padded) == sector1, "Padded JSON does not match original data"
|
|
983
|
+
s1json = s1json_padded
|
|
984
|
+
|
|
985
|
+
encoded_s1 = []
|
|
986
|
+
cbstart = 0
|
|
987
|
+
istart = 0
|
|
988
|
+
while cbstart < len(s1json):
|
|
989
|
+
encoded_s1_block= s1codec.encode(s1json[cbstart:(cbstart+s1codec.block_capacity_bytes)],index_start=istart)
|
|
990
|
+
istart+=len(encoded_s1_block)
|
|
991
|
+
cbstart+=s1codec.block_capacity_bytes
|
|
992
|
+
encoded_s1 += encoded_s1_block
|
|
993
|
+
|
|
994
|
+
#reoncode sector 1 with updated id.
|
|
995
|
+
if length<0:
|
|
996
|
+
#update sector1 id with number of strands in s1.
|
|
997
|
+
return generate_sector1(codec,alphabets,vendor_id,file_meta_data,primer_len,length=len(encoded_s1))
|
|
998
|
+
|
|
999
|
+
#convert to base32 pad with a random base to meet the 70 base requirement for sector 1.
|
|
1000
|
+
s1DNA = [x[0]+bytes(np.random.choice([b"A",b"C",b"T",b"G"])) for x in b32_to_DNA_optimize(encoded_s1,_default_b32_alphabet,_default_b32_alphabet_alt)]
|
|
1001
|
+
s1DNA_with_primers = [sector01.s1fp_ext.encode("utf-8") + x + sector01.s1rp_rc_ext.encode("utf-8") for x in s1DNA]
|
|
1002
|
+
return s1DNA_with_primers
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
s1fp = "ACAACGCTCAAATCAGGG"
|
|
4
|
+
s1rp = "GTGACGGTAGGTGAAATC"
|
|
5
|
+
s1fp_ext = "GCACAACGCTCAAATCAGGG"
|
|
6
|
+
s1rp_ext = "CGGTGACGGTAGGTGAAATC"
|
|
7
|
+
s1rp_rc = "GATTTCACCTACCGTCAC"
|
|
8
|
+
s1rp_rc_ext = "GATTTCACCTACCGTCACCG"
|
|
9
|
+
JSON_template_mandatory = \
|
|
10
|
+
"""
|
|
11
|
+
{
|
|
12
|
+
"id": null,
|
|
13
|
+
"seq": {
|
|
14
|
+
"osizemin": null,
|
|
15
|
+
"osizemedian": null,
|
|
16
|
+
"osizemax": null,
|
|
17
|
+
"orepmin": null,
|
|
18
|
+
"orepmax": null,
|
|
19
|
+
"ocount": null,
|
|
20
|
+
"adapter": false,
|
|
21
|
+
"natural": true,
|
|
22
|
+
"coverageinfo": "30x"
|
|
23
|
+
},
|
|
24
|
+
"codec": {
|
|
25
|
+
"id": "ATGTATAGCGCTACGAGCTGCAGTCATCTGATCTC",
|
|
26
|
+
"vendor": "UW MISL",
|
|
27
|
+
"name": "SyNCODER",
|
|
28
|
+
"ver": null,
|
|
29
|
+
"uri": "http://misl.bio",
|
|
30
|
+
"params": {}
|
|
31
|
+
},
|
|
32
|
+
"vendorid": null
|
|
33
|
+
}
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
JSON_template_optional = \
|
|
37
|
+
"""
|
|
38
|
+
{
|
|
39
|
+
"desc": null,
|
|
40
|
+
"hash": {
|
|
41
|
+
"alg": "SHA256",
|
|
42
|
+
"val": null,
|
|
43
|
+
"addl": {}
|
|
44
|
+
},
|
|
45
|
+
"ts": null,
|
|
46
|
+
"vendor": {
|
|
47
|
+
"name": "UW MISL",
|
|
48
|
+
"uri": "http://misl.bio",
|
|
49
|
+
"contact": null
|
|
50
|
+
},
|
|
51
|
+
"opt": {}
|
|
52
|
+
}
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def get_sector_1_obj():
|
|
56
|
+
return json.loads(JSON_template_mandatory)
|
|
57
|
+
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
#write a test for codec.py
|
|
2
|
-
|
|
3
1
|
import random
|
|
4
2
|
import numpy as np
|
|
5
3
|
from itertools import product
|
|
@@ -44,6 +42,32 @@ def encode_decode(index_type,index_loc:Literal["beginning","middle"]="middle"):
|
|
|
44
42
|
#check inner_n is causing the correct number of bases
|
|
45
43
|
assert len(coded[0]) == 30
|
|
46
44
|
|
|
45
|
+
def test_encode_decode_fast_erase():
|
|
46
|
+
index_type = "inner"
|
|
47
|
+
index_loc:Literal["beginning","middle"]="middle"
|
|
48
|
+
c = codec.BaseNBlockCodec(inner_alphabet_size=32,
|
|
49
|
+
inner_d=5,
|
|
50
|
+
inner_n=30,
|
|
51
|
+
index_type=index_type,
|
|
52
|
+
index_location=index_loc,
|
|
53
|
+
n_strands = 2001,
|
|
54
|
+
n_redundant_strands=100
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
in_text = np.random.bytes( c.block_capacity_bytes )
|
|
58
|
+
coded = c.encode( in_text )
|
|
59
|
+
dnadata = syncoder.b32_to_DNA(coded,syncoder._default_b32_alphabet,syncoder._default_b32_alphabet_alt)
|
|
60
|
+
|
|
61
|
+
random.shuffle(coded)
|
|
62
|
+
coded = coded[:-90] #erase a random strands
|
|
63
|
+
#corrupt_index = random.randint(0,len(coded[0]))
|
|
64
|
+
#coded[0][corrupt_index] = 6
|
|
65
|
+
|
|
66
|
+
out_text = c.decode( coded)[0]
|
|
67
|
+
assert in_text == out_text
|
|
68
|
+
#check inner_n is causing the correct number of bases
|
|
69
|
+
assert len(coded[0]) == 30
|
|
70
|
+
|
|
47
71
|
def test_coded_to_bases():
|
|
48
72
|
#TODO: test optimize too
|
|
49
73
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|