syncoder 0.0.4a2__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.4a2 → syncoder-0.0.6a0}/PKG-INFO +7 -6
- {syncoder-0.0.4a2 → syncoder-0.0.6a0}/README.md +29 -27
- syncoder-0.0.6a0/environments/conda_environment.yml +10 -0
- {syncoder-0.0.4a2 → syncoder-0.0.6a0}/environments/conda_environment_dev.yml +7 -6
- syncoder-0.0.6a0/environments/conda_environment_pipeline.yml +14 -0
- syncoder-0.0.6a0/environments/conda_environment_test.yml +15 -0
- {syncoder-0.0.4a2 → syncoder-0.0.6a0}/pyproject.toml +33 -34
- {syncoder-0.0.4a2 → syncoder-0.0.6a0}/src/syncoder/__init__.py +6 -6
- {syncoder-0.0.4a2 → syncoder-0.0.6a0}/src/syncoder/codec.py +363 -89
- syncoder-0.0.6a0/src/syncoder/sector01.py +57 -0
- {syncoder-0.0.4a2 → syncoder-0.0.6a0}/tests/test_codec_pytest.py +45 -3
- syncoder-0.0.4a2/environments/conda_environment.yml +0 -12
- syncoder-0.0.4a2/environments/conda_environment_test.yml +0 -14
- {syncoder-0.0.4a2 → syncoder-0.0.6a0}/.gitignore +0 -0
- {syncoder-0.0.4a2 → syncoder-0.0.6a0}/.vscode/.gitignore +0 -0
- {syncoder-0.0.4a2 → syncoder-0.0.6a0}/LICENSE +0 -0
- {syncoder-0.0.4a2 → 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.
|
|
@@ -2,19 +2,20 @@ name: codec-dev
|
|
|
2
2
|
channels:
|
|
3
3
|
- conda-forge
|
|
4
4
|
dependencies:
|
|
5
|
-
- python>=3.
|
|
6
|
-
-
|
|
5
|
+
- python>=3.13
|
|
6
|
+
- pip
|
|
7
|
+
- "libblas=*=*mkl"
|
|
8
|
+
- numpy
|
|
7
9
|
- matplotlib=3.*
|
|
10
|
+
- numba=0.62.* # version pinned for galois 0.4.8
|
|
8
11
|
- pytest
|
|
9
|
-
- reedsolo=1.7.*
|
|
10
|
-
- pip=24.*
|
|
11
12
|
- sphinx=7.*
|
|
12
13
|
- sphinx_rtd_theme
|
|
13
14
|
- biopython
|
|
14
15
|
- jupyter
|
|
15
16
|
- ipykernel
|
|
17
|
+
- twine
|
|
16
18
|
- pip:
|
|
17
|
-
- galois
|
|
19
|
+
- galois>=0.4.8
|
|
18
20
|
- build
|
|
19
|
-
- twine
|
|
20
21
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
name: codec-test
|
|
2
|
+
channels:
|
|
3
|
+
- conda-forge
|
|
4
|
+
dependencies:
|
|
5
|
+
- python>=3.13
|
|
6
|
+
- pip
|
|
7
|
+
- numpy=2.2.6
|
|
8
|
+
- numba=0.62.* # version pinned for galois 0.4.8
|
|
9
|
+
- pytest
|
|
10
|
+
- sphinx=7.*
|
|
11
|
+
- sphinx_rtd_theme
|
|
12
|
+
- twine
|
|
13
|
+
- pip:
|
|
14
|
+
- galois>=0.4.8
|
|
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
|
|
|
@@ -124,7 +113,9 @@ class BaseNBlockCodec:
|
|
|
124
113
|
inner_n:Optional[int]=None,
|
|
125
114
|
n_redundant_strands:int=5,
|
|
126
115
|
n_strands:int=100,
|
|
127
|
-
max_strand_index:Optional[int]=None
|
|
116
|
+
max_strand_index:Optional[int]=None,
|
|
117
|
+
index_type:Literal["binary", "inner symbol", "inner"]="binary",
|
|
118
|
+
index_location:Literal["middle", "beginning", "end"]="middle"):
|
|
128
119
|
"""
|
|
129
120
|
Initialize the Codec object.
|
|
130
121
|
|
|
@@ -135,6 +126,15 @@ class BaseNBlockCodec:
|
|
|
135
126
|
n_redundant_strands (int): Number of redundant strands.
|
|
136
127
|
n_strands (int): Number of "strands" stored by this codec including redundant strands.
|
|
137
128
|
max_strand_index (Optional[int]): Maximum guaranteed index. Default: n_strands.
|
|
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.
|
|
134
|
+
index_location (str): Location of the index within the inner code. Options: "middle", "beginning", "end". Default: "middle".
|
|
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)
|
|
138
138
|
"""
|
|
139
139
|
if n_strands<=255:
|
|
140
140
|
self.outer_alphabet_size_bytes = 1
|
|
@@ -147,6 +147,21 @@ class BaseNBlockCodec:
|
|
|
147
147
|
max_strand_index = n_strands
|
|
148
148
|
if max_strand_index<n_strands:
|
|
149
149
|
raise ValueError("max_strand_index must be greater than or equal to n_strands.")
|
|
150
|
+
self.max_strand_index = max_strand_index
|
|
151
|
+
|
|
152
|
+
if index_type not in {"binary","inner symbol","inner"}:
|
|
153
|
+
raise ValueError("index_type must be 'binary' or 'inner symbol'.")
|
|
154
|
+
#"inner" and "inner symbol" are synomymous.
|
|
155
|
+
self.index_type = "inner" if index_type == "inner symbol" else index_type
|
|
156
|
+
|
|
157
|
+
if index_location in {"middle","beginning","end"}:
|
|
158
|
+
self.index_location = index_location
|
|
159
|
+
if index_location == "end":
|
|
160
|
+
raise NotImplementedError("index_location 'end' is not implemented.")
|
|
161
|
+
if index_location != "middle" and index_type == "binary":
|
|
162
|
+
raise ValueError("index_location 'beginning' or 'end' not supported for index_type 'binary'.")
|
|
163
|
+
else:
|
|
164
|
+
raise ValueError("index_location must be 'middle', 'beginning', or 'end'.")
|
|
150
165
|
|
|
151
166
|
self.n_message_strands = n_strands - n_redundant_strands
|
|
152
167
|
|
|
@@ -157,6 +172,7 @@ class BaseNBlockCodec:
|
|
|
157
172
|
# make sure extra sequence numbers are not added to inner blocks. and or maybe add a
|
|
158
173
|
# parameter for the number of inner blocks per strand.
|
|
159
174
|
assert inner_n<inner_alphabet_size
|
|
175
|
+
self.inner_n = inner_n
|
|
160
176
|
|
|
161
177
|
self.inner_coder = galois.ReedSolomon(n=inner_alphabet_size-1,d=inner_d,field=galois.GF(inner_alphabet_size))
|
|
162
178
|
# in units of symbols (there are q=inner_alphabet_size possible symbols)
|
|
@@ -173,29 +189,127 @@ class BaseNBlockCodec:
|
|
|
173
189
|
self.inner_k = k
|
|
174
190
|
q = self.inner_coder.field.order
|
|
175
191
|
|
|
176
|
-
#number of bytes needed to store the index
|
|
177
|
-
self.index_bits = np.ceil(np.log2(max_strand_index)).astype(int)
|
|
178
|
-
logging.debug("bits per strand used for indexing: {}".format(self.index_bits))
|
|
179
192
|
|
|
180
193
|
#message length in bytes (without index)
|
|
181
|
-
|
|
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))
|
|
198
|
+
self.data_chunk_size = int(k*np.log2(q)-self.index_bits) // 8
|
|
199
|
+
elif self.index_type == "inner":
|
|
200
|
+
self.index_symbols = len(_int_to_baseN(max_strand_index,q))
|
|
201
|
+
self.data_symbols = k-self.index_symbols
|
|
202
|
+
self.data_chunk_size = int(self.data_symbols*np.log2(q)) // 8
|
|
203
|
+
assert k == self.data_symbols + self.index_symbols
|
|
204
|
+
logging.debug("data chunk size in symbols: {}".format(self.data_symbols))
|
|
205
|
+
logging.debug(f"number of inner symbols: {k}; index symbols: {self.index_symbols}")
|
|
206
|
+
|
|
182
207
|
logging.debug( "strand capacity in bytes: {}".format(self.data_chunk_size) )
|
|
208
|
+
#todo: only execute if logging level is debug or warning
|
|
209
|
+
self.__compute_waste()
|
|
210
|
+
|
|
211
|
+
self.block_capacity_bytes = self.data_chunk_size*(self.n_strands - n_redundant_strands)
|
|
212
|
+
logging.debug( "codec message size: {} bytes".format(self.block_capacity_bytes) )
|
|
183
213
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
214
|
+
#init the outer coder
|
|
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)))
|
|
223
|
+
|
|
224
|
+
def __compute_waste(self):
|
|
225
|
+
k = self.inner_k
|
|
226
|
+
q = self.inner_coder.field.order
|
|
227
|
+
if self.index_type == "binary":
|
|
228
|
+
_wasted_bits = k*np.log2(q) - (self.data_chunk_size*8+self.index_bits )
|
|
229
|
+
logging.debug( "{} bits wasted per strand".format( _wasted_bits ) )
|
|
230
|
+
logging.debug( "Maximum strand index (using wasted bits): {}".format(2**(int(self.index_bits+_wasted_bits))))
|
|
231
|
+
_wasted_symbols = _wasted_bits/np.log2(q)
|
|
232
|
+
elif self.index_type == "inner":
|
|
233
|
+
data_chunk_symbols_used = self.data_chunk_size*8/np.log2(q)
|
|
234
|
+
_wasted_symbols = self.data_symbols - data_chunk_symbols_used
|
|
235
|
+
logging.debug( f"Maximum strand index:{q**self.index_symbols}")
|
|
236
|
+
else:
|
|
237
|
+
raise ValueError("Unknown index type: {}".format(self.index_type)) #should be impossible to get here.
|
|
188
238
|
if _wasted_symbols>=1:
|
|
189
239
|
logging.warning( "{} symbols wasted per strand. Consider decreasing inner_n.".format( _wasted_symbols) )
|
|
190
240
|
else:
|
|
191
241
|
logging.debug("{} symbols wasted per strand".format( _wasted_symbols))
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
self.block_capacity_bytes = self.data_chunk_size*(self.n_strands - n_redundant_strands)
|
|
195
|
-
logging.debug( "codec message size: {} bytes".format(self.block_capacity_bytes) )
|
|
196
242
|
|
|
197
|
-
|
|
198
|
-
|
|
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
|
|
199
313
|
|
|
200
314
|
def encode(self,data:bytes,index_start:int=0)->list[list[int]]:
|
|
201
315
|
""" Encodes the given data using the outer and inner codes.
|
|
@@ -231,23 +345,27 @@ class BaseNBlockCodec:
|
|
|
231
345
|
# |<rrrrrrrrrrrrrrrrr self.data_chunk_size rrrrrrrrrrrrr>|
|
|
232
346
|
# ...
|
|
233
347
|
# |<rrrrrrrrrrrrrrrrr self.data_chunk_size rrrrrrrrrrrrr>|
|
|
234
|
-
# 3. inner code (R= inner code redundancy applied to rows
|
|
235
|
-
#
|
|
236
|
-
#
|
|
348
|
+
# 3. inner code (R= inner code redundancy applied to rows)
|
|
349
|
+
# (* = inner code symbol)
|
|
350
|
+
# (@= binary index concatenated to data bits)
|
|
351
|
+
# (a= binary index concatenated to outer code redundancy bits)
|
|
352
|
+
# When index type is inner code, @ and a are full symbols.
|
|
353
|
+
# |<***************** self.data_chunk_size *************@@@RRRRRRRR>|
|
|
354
|
+
# |<***************** self.data_chunk_size *************@@@RRRRRRRR>|
|
|
237
355
|
# ...
|
|
238
|
-
# |<***************** self.data_chunk_size
|
|
239
|
-
# |<rrrrrrrrrrrrrrrrr self.data_chunk_size
|
|
356
|
+
# |<***************** self.data_chunk_size *************@@@RRRRRRRR>|
|
|
357
|
+
# |<rrrrrrrrrrrrrrrrr self.data_chunk_size rrrrrrrrrrrrraaaRRRRRRRR>|
|
|
240
358
|
# ...
|
|
241
|
-
# |<rrrrrrrrrrrrrrrrr self.data_chunk_size
|
|
359
|
+
# |<rrrrrrrrrrrrrrrrr self.data_chunk_size rrrrrrrrrrrrraaaRRRRRRRR>|
|
|
242
360
|
|
|
243
361
|
#1. Reshape data
|
|
244
362
|
logging.debug("applying outer code to {} bytes".format(len(data)))
|
|
245
363
|
# reshape data so columns can be coded independently.
|
|
246
|
-
if self.
|
|
364
|
+
if self.outer_alphabet_size_bytes==1:
|
|
247
365
|
data_np = np.frombuffer(data,dtype=np.uint8)
|
|
248
366
|
assert len(data_np)%self.data_chunk_size == 0
|
|
249
367
|
data_np = data_np.reshape(-1,self.data_chunk_size)
|
|
250
|
-
elif self.
|
|
368
|
+
elif self.outer_alphabet_size_bytes==2:
|
|
251
369
|
data_np = np.frombuffer(data,dtype=np.uint16)
|
|
252
370
|
if self.data_chunk_size%2!=0:
|
|
253
371
|
raise ValueError("data_chunk_size must be even for 16 bit outer code.")
|
|
@@ -259,7 +377,8 @@ class BaseNBlockCodec:
|
|
|
259
377
|
raise ValueError("Unsupported outer code byte size.")
|
|
260
378
|
|
|
261
379
|
#2. apply the outer code (encode columns of data)
|
|
262
|
-
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
|
+
|
|
263
382
|
data_enc_chunks = data_enc_cols.transpose()
|
|
264
383
|
|
|
265
384
|
logging.debug("outer coded strands: {}".format(data_enc_chunks.shape[0]))
|
|
@@ -273,11 +392,23 @@ class BaseNBlockCodec:
|
|
|
273
392
|
d = chunk.tobytes()
|
|
274
393
|
#use little endian to match x86/x64 byte order and avoid needing to pad byte string.
|
|
275
394
|
chunk = int.from_bytes(d,'little')
|
|
276
|
-
|
|
277
|
-
|
|
395
|
+
if self.index_type == "binary":
|
|
396
|
+
#add index to end (MS bits)
|
|
397
|
+
chunk += index<<(self.data_chunk_size*8)
|
|
398
|
+
|
|
399
|
+
#but.. we still need to pad out the symbol stream.
|
|
400
|
+
chunk = _int_to_baseN(chunk,self.inner_coder.field.order,length=self.inner_k)
|
|
401
|
+
elif self.index_type == "inner":
|
|
402
|
+
chunk = _int_to_baseN(chunk,self.inner_coder.field.order,length=self.data_symbols)
|
|
403
|
+
index_symbols = _int_to_baseN(index,self.inner_coder.field.order,length=self.index_symbols)
|
|
404
|
+
if self.index_location == "middle":
|
|
405
|
+
chunk += index_symbols
|
|
406
|
+
elif self.index_location == "beginning":
|
|
407
|
+
chunk = index_symbols + chunk
|
|
408
|
+
else:
|
|
409
|
+
raise NotImplementedError("index_location 'end' is not implemented.")
|
|
410
|
+
|
|
278
411
|
index += 1
|
|
279
|
-
#but.. we still need to pad out the symbol stream.
|
|
280
|
-
chunk = _int_to_baseN(chunk,self.inner_coder.field.order,length=self.inner_k)
|
|
281
412
|
chunks.append(chunk)
|
|
282
413
|
|
|
283
414
|
#apply inner code
|
|
@@ -285,16 +416,17 @@ class BaseNBlockCodec:
|
|
|
285
416
|
|
|
286
417
|
return ic_chunks
|
|
287
418
|
|
|
288
|
-
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]:
|
|
289
420
|
"""
|
|
290
421
|
Decodes the given data.
|
|
291
422
|
|
|
292
423
|
Args:
|
|
293
424
|
data (list[list[int]]): The encoded data to be decoded.
|
|
294
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.
|
|
295
427
|
|
|
296
428
|
Returns:
|
|
297
|
-
bytes: The decoded data.
|
|
429
|
+
bytes|None: The decoded data. Or None if decoding failed.
|
|
298
430
|
outer erasures: list of erasures in outer code.
|
|
299
431
|
outer errors: list of errors in outer code.
|
|
300
432
|
inner errors: list of number of errors in inner code by strand index. -1 -> strand not present.
|
|
@@ -302,28 +434,49 @@ class BaseNBlockCodec:
|
|
|
302
434
|
Raises:
|
|
303
435
|
None
|
|
304
436
|
"""
|
|
437
|
+
if n_strands is None:
|
|
438
|
+
n_strands = self.n_strands
|
|
439
|
+
|
|
305
440
|
# inner decode
|
|
306
441
|
## TODO: handle erasures in inner code.
|
|
307
442
|
|
|
308
443
|
# I'm sure it'll be a (FieldArray,int) ----------------V
|
|
309
|
-
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
|
|
310
445
|
|
|
311
446
|
|
|
312
447
|
# handle outer decode
|
|
313
|
-
ordered_chunks:list[bytes|None] = [None] *
|
|
314
|
-
chunk_errors:list[int] = [-1] *
|
|
448
|
+
ordered_chunks:list[bytes|None] = [None] * n_strands
|
|
449
|
+
chunk_errors:list[int] = [-1] * n_strands
|
|
315
450
|
for chunk in chunked_data:
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
|
454
|
+
if self.index_type == "binary":
|
|
455
|
+
chunk_int = _baseN_to_int(chunk[0].tolist(), self.inner_coder.field.order)
|
|
456
|
+
chunk_index = (chunk_int >> (self.data_chunk_size * 8)) - index_start
|
|
457
|
+
_mask = (2 ** (self.data_chunk_size * 8)) - 1
|
|
458
|
+
chunk_int = chunk_int & _mask
|
|
459
|
+
elif self.index_type == "inner":
|
|
460
|
+
if self.index_location == "middle":
|
|
461
|
+
chunk_int = _baseN_to_int(chunk[0][:self.data_symbols].tolist(),self.inner_coder.field.order)
|
|
462
|
+
chunk_index = _baseN_to_int(chunk[0][self.data_symbols:].tolist(),self.inner_coder.field.order) - index_start
|
|
463
|
+
elif self.index_location == "beginning":
|
|
464
|
+
chunk_index = _baseN_to_int(chunk[0][:-self.data_symbols].tolist(),self.inner_coder.field.order) - index_start
|
|
465
|
+
chunk_int = _baseN_to_int(chunk[0][-self.data_symbols:].tolist(),self.inner_coder.field.order)
|
|
466
|
+
else:
|
|
467
|
+
raise NotImplementedError("index_location 'end' is not implemented.")
|
|
468
|
+
else:
|
|
469
|
+
raise ValueError("Unknown index type: {}".format(self.index_type)) #should be impossible to get here.
|
|
470
|
+
#this can be to big sometimes. TODO handle more gracefully
|
|
322
471
|
try:
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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))
|
|
327
480
|
|
|
328
481
|
# identify erasures
|
|
329
482
|
erasures = []
|
|
@@ -334,20 +487,32 @@ class BaseNBlockCodec:
|
|
|
334
487
|
#we're all bytes now, but the checker can't tell that.
|
|
335
488
|
ordered_chunks_bytes:list[bytes] = typing.cast(list[bytes],ordered_chunks)
|
|
336
489
|
|
|
337
|
-
|
|
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:
|
|
338
494
|
outer_dtype = np.uint8
|
|
339
|
-
elif self.
|
|
495
|
+
elif self.outer_alphabet_size_bytes==2:
|
|
340
496
|
outer_dtype = np.uint16
|
|
341
497
|
else:
|
|
342
498
|
raise ValueError("Unsupported outer code byte size.")
|
|
343
|
-
data_np_encoded = np.array([np.frombuffer(c, dtype=outer_dtype) for c in ordered_chunks_bytes]).transpose()
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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()
|
|
347
514
|
|
|
348
|
-
|
|
349
|
-
logging.info("erasures (lost) chunks: {}".format(erasures))
|
|
350
|
-
data_errata_col_pos = [e[2] for e in data_decoded_results]
|
|
515
|
+
data_decoded = data_decoded_chunks.flatten().tobytes()
|
|
351
516
|
|
|
352
517
|
errors = []
|
|
353
518
|
for i, pos in enumerate(data_errata_col_pos):
|
|
@@ -361,53 +526,74 @@ class BaseNBlockCodec:
|
|
|
361
526
|
if e[0] in erasures:
|
|
362
527
|
pass
|
|
363
528
|
else:
|
|
364
|
-
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]
|
|
365
532
|
|
|
366
|
-
return data_decoded, erasures, errors, chunk_errors
|
|
367
533
|
|
|
368
|
-
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:
|
|
369
535
|
"""
|
|
370
536
|
Extracts the index from the given DNA strand.
|
|
371
537
|
|
|
372
538
|
Args:
|
|
373
539
|
dna (str|bytes): The input DNA strand from which to extract the index.
|
|
374
|
-
error_check (bool, optional): If True, the inner_codec will be used to
|
|
375
|
-
attempt to check for errors.
|
|
376
|
-
|
|
377
|
-
|
|
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.
|
|
378
546
|
words (list[str]): The primary set of base words used, in order, for encoding.
|
|
379
547
|
alternate_words (list[str]): The alternate (aka synonymous) set of words used, in order, for encoding.
|
|
380
548
|
|
|
381
549
|
Returns:
|
|
382
550
|
int: The index. or -1 if error_check==True and unrecoverable error found.
|
|
383
551
|
"""
|
|
384
|
-
|
|
552
|
+
symbol_len:int = len(words[0])
|
|
385
553
|
if isinstance(dna,bytes):
|
|
386
554
|
dna= dna.decode()
|
|
387
555
|
dna_s:str = cast(str,dna)
|
|
388
|
-
|
|
389
|
-
if padding == "right":
|
|
390
|
-
_p = self.inner_k*word_len - len(dna_s)
|
|
391
|
-
if _p>0: #if i have less than a full codeword of bases
|
|
392
|
-
dna_s = dna_s + "A"*_p
|
|
393
|
-
elif padding == "left":
|
|
394
|
-
_p = self.inner_k*word_len - len(dna_s)
|
|
395
|
-
if _p>0: #if i have less than a full codeword of bases
|
|
396
|
-
dna_s = "A"*_p + dna_s
|
|
397
556
|
|
|
557
|
+
num_missing = self.inner_k*symbol_len - len(dna_s) # in DNA bases
|
|
558
|
+
if num_missing>0: #if i have less than a full codeword of bases
|
|
559
|
+
if padding == "right":
|
|
560
|
+
dna_s = dna_s + "A"*num_missing
|
|
561
|
+
elif padding == "left":
|
|
562
|
+
dna_s = "A"*num_missing + dna_s
|
|
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)
|
|
582
|
+
#This could fail if padding makes a symbol not in words or alternate_words.
|
|
398
583
|
data = dna_to_bN([dna_s],words,alternate_words)[0]
|
|
399
584
|
|
|
400
|
-
return self.extract_index(data,
|
|
585
|
+
return self.extract_index(data,ec,erasures)
|
|
401
586
|
|
|
402
|
-
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:
|
|
403
588
|
"""
|
|
404
|
-
Extracts the index from the given
|
|
589
|
+
Extracts the index from the given inner code sequence.
|
|
405
590
|
|
|
406
591
|
Args:
|
|
407
592
|
data list[int]: The input data from which to extract the index.
|
|
408
593
|
If a list[int] is given, it is assumed be be inner code symbols.
|
|
409
594
|
error_check (bool, optional): If True, the inner_codec will be used to
|
|
410
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.
|
|
411
597
|
|
|
412
598
|
Returns:
|
|
413
599
|
int: The index. or -1 if error_check==True and unrecoverable error found.
|
|
@@ -415,11 +601,23 @@ class BaseNBlockCodec:
|
|
|
415
601
|
if error_check == False:
|
|
416
602
|
data = data[0:self.inner_k] #extract message
|
|
417
603
|
else:
|
|
418
|
-
|
|
604
|
+
data_gf, n_corrected = self.inner_coder.decode(data,errors=True,erasures=erasures) #type:ignore
|
|
605
|
+
data = data_gf.tolist()
|
|
419
606
|
if n_corrected < 0:
|
|
420
607
|
return -1
|
|
421
|
-
|
|
422
|
-
|
|
608
|
+
if self.index_type == "binary":
|
|
609
|
+
chunk_int = _baseN_to_int(data, self.inner_coder.field.order)
|
|
610
|
+
chunk_index = (chunk_int >> (self.data_chunk_size * 8))
|
|
611
|
+
elif self.index_type == "inner":
|
|
612
|
+
if self.index_location == "middle":
|
|
613
|
+
chunk_index_baseN = data[self.data_symbols:]
|
|
614
|
+
elif self.index_location == "beginning":
|
|
615
|
+
chunk_index_baseN = data[:-self.data_symbols]
|
|
616
|
+
else:
|
|
617
|
+
raise NotImplementedError("index_location 'end' is not implemented.")
|
|
618
|
+
chunk_index = _baseN_to_int(chunk_index_baseN, self.inner_coder.field.order)
|
|
619
|
+
else:
|
|
620
|
+
raise ValueError("Unknown index type: {}".format(self.index_type)) #should be impossible to get here.
|
|
423
621
|
return chunk_index
|
|
424
622
|
|
|
425
623
|
|
|
@@ -465,7 +663,7 @@ def longest_binder(a:str,b:str):
|
|
|
465
663
|
|
|
466
664
|
|
|
467
665
|
|
|
468
|
-
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]]:
|
|
469
667
|
"""
|
|
470
668
|
TODO: rename to bN_to_DNA_optimize, and add test.
|
|
471
669
|
file_data: 2d list of integral types (symbols)
|
|
@@ -726,3 +924,79 @@ def remove_bytes(data:bytes, insert:int|bytes, chunk_len:int, num_inserts:int) -
|
|
|
726
924
|
del data[ins_index:ins_index + insert_len]
|
|
727
925
|
return bytes(data)
|
|
728
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,8 +1,7 @@
|
|
|
1
|
-
#write a test for codec.py
|
|
2
|
-
|
|
3
1
|
import random
|
|
4
2
|
import numpy as np
|
|
5
3
|
from itertools import product
|
|
4
|
+
from typing import Literal
|
|
6
5
|
|
|
7
6
|
import syncoder
|
|
8
7
|
codec = syncoder
|
|
@@ -12,11 +11,27 @@ codec = syncoder
|
|
|
12
11
|
#numba_logger.setLevel(logging.WARNING)
|
|
13
12
|
|
|
14
13
|
def test_encode_decode():
|
|
15
|
-
|
|
14
|
+
encode_decode(index_type="binary")
|
|
15
|
+
encode_decode(index_type="inner")
|
|
16
|
+
encode_decode(index_type="inner", index_loc="beginning")
|
|
17
|
+
|
|
18
|
+
def encode_decode(index_type,index_loc:Literal["beginning","middle"]="middle"):
|
|
19
|
+
c = codec.BaseNBlockCodec(inner_alphabet_size=32,
|
|
20
|
+
inner_d=5,
|
|
21
|
+
inner_n=30,
|
|
22
|
+
index_type=index_type,
|
|
23
|
+
index_location=index_loc)
|
|
16
24
|
|
|
17
25
|
in_text = (codec.lipsum + codec.lipsum)[0:c.block_capacity_bytes]
|
|
18
26
|
|
|
19
27
|
coded = c.encode( in_text )
|
|
28
|
+
assert c.extract_index(coded[27]) == 27 #test extract index while we're at it
|
|
29
|
+
dnadata = syncoder.b32_to_DNA(coded,syncoder._default_b32_alphabet,syncoder._default_b32_alphabet_alt)
|
|
30
|
+
assert c.extract_index_dna(dnadata[27],
|
|
31
|
+
words = syncoder._default_b32_alphabet,
|
|
32
|
+
alternate_words= syncoder._default_b32_alphabet_alt,
|
|
33
|
+
error_check=True) == 27
|
|
34
|
+
|
|
20
35
|
random.shuffle(coded)
|
|
21
36
|
#coded = coded[:-4] #erase a random strands
|
|
22
37
|
corrupt_index = random.randint(0,len(coded[0]))
|
|
@@ -27,6 +42,32 @@ def test_encode_decode():
|
|
|
27
42
|
#check inner_n is causing the correct number of bases
|
|
28
43
|
assert len(coded[0]) == 30
|
|
29
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
|
+
|
|
30
71
|
def test_coded_to_bases():
|
|
31
72
|
#TODO: test optimize too
|
|
32
73
|
|
|
@@ -76,6 +117,7 @@ def test_dna_to_bytes():
|
|
|
76
117
|
dna_bytes, mask = codec.dna_to_bytes(DNA,alphabet,alphabet_alt)
|
|
77
118
|
|
|
78
119
|
dna_baseN = _int_to_baseN(int.from_bytes(dna_bytes,"little"),len(alphabet))
|
|
120
|
+
#TODO: this can fail but only for some inputs randomly selected lookinto why.
|
|
79
121
|
assert dna_baseN == answer32
|
|
80
122
|
|
|
81
123
|
_t = np.array(dna_baseN)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|