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.
@@ -1,16 +1,15 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: syncoder
3
- Version: 0.0.4a2
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.9
11
- Requires-Dist: galois>=0.4
12
- Requires-Dist: numpy>=1.20
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
- ## Installation
10
- `codec.py` Can be included in a project and used as a local module or installed and used from `pip` :
11
- ```
12
- pip install --pre syncoder
13
- ```
14
- Note the requirement for the `--pre` flag as there is no official post release yet.
15
-
16
- ## Usage
17
- To be completed. For now see `tests/test_codec_pytest.py` for examples
18
-
19
- ## Testing
20
- It is sufficient to run
21
- ```
22
- pytest
23
- ```
24
- from the root directory. Currently coverage is only for features and parameters in active use. More tests to come.
25
-
26
- ## License
27
- BSD-3 clause. see `LICENSE` file in this directory.
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.
@@ -0,0 +1,10 @@
1
+ name: codec-user
2
+ channels:
3
+ - conda-forge
4
+ dependencies:
5
+ - python>=3.13
6
+ - numpy=2.*
7
+ - numba=0.62.*
8
+ - pip:
9
+ - galois>=0.4.8
10
+
@@ -2,19 +2,20 @@ name: codec-dev
2
2
  channels:
3
3
  - conda-forge
4
4
  dependencies:
5
- - python>=3.12
6
- - numpy=2.*
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==0.4.*
19
+ - galois>=0.4.8
18
20
  - build
19
- - twine
20
21
 
@@ -0,0 +1,14 @@
1
+ name: codec-pipeline
2
+ channels:
3
+ - bioconda
4
+ - conda-forge
5
+ dependencies:
6
+ - bioconda::pandaseq
7
+ - bioconda::cutadapt
8
+ - bioconda::vsearch
9
+ - python>=3.13
10
+ - numpy=2.*
11
+ - numba=0.62.*
12
+ - pip:
13
+ - galois>=0.4.8
14
+
@@ -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>=1.20",
6
- "galois>=0.4",
7
- "reedsolo>=1.7"
8
- ]
9
- description = "A codec for DNA data storage"
10
- authors = [
11
- { name="Christopher Takahashi", email="cnt@uw.edu" }
12
- ]
13
- readme = "README.md"
14
- requires-python = ">=3.9"
15
- license = "BSD-3-Clause"
16
- license-files = ["LICENSE"]
17
-
18
- [project.urls]
19
- Homepage = "https://example.com/TBD"
20
- Issues = "https://example.com/TBD/issues"
21
-
22
- [tool.setuptools]
23
- packages = ["syncoder"]
24
-
25
- [build-system]
26
- requires = ["hatchling >= 1.26"]
27
- build-backend = "hatchling.build"
28
-
29
- [tool.pytest.ini_options]
30
- testpaths = [
31
- "tests"
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.4a2"
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
- self.data_chunk_size = int(k*np.log2(q)-self.index_bits) // 8
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
- _wasted_bits = k*np.log2(q) - (self.data_chunk_size*8+self.index_bits )
185
- logging.debug( "{} bits wasted per strand".format( _wasted_bits ) )
186
- logging.debug( "Maximum strand index (using wasted bits): {}".format(2**(int(self.index_bits+_wasted_bits))))
187
- _wasted_symbols = _wasted_bits/np.log2(q)
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
- #init the outer coder
198
- self.outer_coder = reedsolo.RSCodec(n_redundant_strands, c_exp=8*self.outer_alphabet_size_bytes)
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, * = inner code symbol)
235
- # |<***************** self.data_chunk_size *************RRRRRRRR>|
236
- # |<***************** self.data_chunk_size *************RRRRRRRR>|
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 *************RRRRRRRR>|
239
- # |<rrrrrrrrrrrrrrrrr self.data_chunk_size rrrrrrrrrrrrrRRRRRRRR>|
356
+ # |<***************** self.data_chunk_size *************@@@RRRRRRRR>|
357
+ # |<rrrrrrrrrrrrrrrrr self.data_chunk_size rrrrrrrrrrrrraaaRRRRRRRR>|
240
358
  # ...
241
- # |<rrrrrrrrrrrrrrrrr self.data_chunk_size rrrrrrrrrrrrrRRRRRRRR>|
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.outer_coder.c_exp==8:
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.outer_coder.c_exp==16:
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.outer_coder.encode(dc) for dc in data_np.transpose()],dtype=data_np.dtype)
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
- #add index to end (MS bits)
277
- chunk += index<<(self.data_chunk_size*8)
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] * self.n_strands
314
- chunk_errors:list[int] = [-1] * self.n_strands
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
- chunk_int = _baseN_to_int(chunk[0].tolist(), self.inner_coder.field.order)
317
- chunk_index = (chunk_int >> (self.data_chunk_size * 8)) - index_start
318
- _mask = (2 ** (self.data_chunk_size * 8)) - 1
319
- chunk_int = chunk_int & _mask
320
- chunk_bytes:bytes = chunk_int.to_bytes(self.data_chunk_size, 'little')
321
- #print(chunk_index)
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
- ordered_chunks[chunk_index] = chunk_bytes
324
- chunk_errors[chunk_index] = chunk[1]
325
- except IndexError:
326
- logging.info("strand index out of bounds:" + str(chunk_index))
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
- if self.outer_coder.c_exp==8:
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.outer_coder.c_exp==16:
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
- data_decoded_results = [self.outer_coder.decode(dc, erase_pos=erasures) for dc in data_np_encoded]
345
- data_decoded_chunks = np.array([d[0] for d in data_decoded_results],dtype=outer_dtype).transpose()
346
- data_decoded = data_decoded_chunks.flatten().tobytes()
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
- # report erasures (lost strands) in outer code
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.warning("Outer code errors found on strand {}, byte {}.".format(*e))
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. Defaults to False.
376
- padding Literal["left","right"], optional): Assume any missing bases are left or right of index. Defaults to "right".
377
- Note padded sequences are unlikely to error correct properly.
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
- word_len:int = len(words[0])
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,error_check)
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 DNA strand.
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
- data, n_corrected = self.inner_coder.decode(data,errors=True)[0].tolist()
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
- chunk_int = _baseN_to_int(data, self.inner_coder.field.order)
422
- chunk_index = (chunk_int >> (self.data_chunk_size * 8))
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
- c = codec.BaseNBlockCodec(inner_alphabet_size=32,inner_d=5,inner_n=30)
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)
@@ -1,12 +0,0 @@
1
- name: codec-user
2
- channels:
3
- - conda-forge
4
- dependencies:
5
- - python>=3.12
6
- - numpy=2.*
7
- - pytest
8
- - reedsolo=1.7.*
9
- - pip=24.*
10
- - pip:
11
- - galois==0.4.*
12
-
@@ -1,14 +0,0 @@
1
- name: codec-test
2
- channels:
3
- - conda-forge
4
- dependencies:
5
- - python>=3.12
6
- - numpy=2.*
7
- - pytest
8
- - reedsolo=1.7.*
9
- - pip=24.*
10
- - sphinx=7.*
11
- - sphinx_rtd_theme
12
- - pip:
13
- - galois==0.4.*
14
-
File without changes
File without changes