syncoder 0.0.5a0__tar.gz → 0.0.6a1__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.5a0
3
+ Version: 0.0.6a1
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.
@@ -3,8 +3,8 @@ channels:
3
3
  - conda-forge
4
4
  dependencies:
5
5
  - python>=3.13
6
- - numpy=2.2.6
7
- - numba=0.61.*
6
+ - numpy=2.*
7
+ - numba=0.62.*
8
8
  - pip:
9
- - galois>=0.4.6
9
+ - galois>=0.4.8
10
10
 
@@ -5,9 +5,9 @@ dependencies:
5
5
  - python>=3.13
6
6
  - pip
7
7
  - "libblas=*=*mkl"
8
- - numpy=2.2.6 # version pinned for galois 0.4.6
8
+ - numpy
9
9
  - matplotlib=3.*
10
- - numba=0.61.* # version pinned for galois 0.4.6
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.6
19
+ - galois>=0.4.8
20
20
  - build
21
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
+
@@ -4,12 +4,12 @@ channels:
4
4
  dependencies:
5
5
  - python>=3.13
6
6
  - pip
7
- - numpy=2.2.6 # version pinned for galois 0.4.6
8
- - numba=0.61.* # version pinned for galois 0.4.6
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.6
14
+ - galois>=0.4.8
15
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.5a0"
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.6a1"
@@ -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
 
@@ -126,7 +115,8 @@ class BaseNBlockCodec:
126
115
  n_strands:int=100,
127
116
  max_strand_index:Optional[int]=None,
128
117
  index_type:Literal["binary", "inner symbol", "inner"]="binary",
129
- index_location:Literal["middle", "beginning", "end"]="middle"):
118
+ index_location:Literal["middle", "beginning", "end"]="middle",
119
+ lazy_outer:bool = False):
130
120
  """
131
121
  Initialize the Codec object.
132
122
 
@@ -138,15 +128,15 @@ class BaseNBlockCodec:
138
128
  n_strands (int): Number of "strands" stored by this codec including redundant strands.
139
129
  max_strand_index (Optional[int]): Maximum guaranteed index. Default: n_strands.
140
130
  index_type (str): Type of index to use. Options: "binary", "inner symbol", "inner". Default: "binary".
131
+ index_type "binary" integrates the index into the data stream as a binary number.
132
+ This may help packing efficiency.
133
+ index_type "inner symbol"/"inner" integrates the index inner code symbol symbols.
134
+ This may help packing efficiency and always simplifies index extraction.
141
135
  index_location (str): Location of the index within the inner code. Options: "middle", "beginning", "end". Default: "middle".
142
-
143
- index_type "binary" integrates the index into the data stream as a binary number.
144
- This may help packing efficiency.
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)
136
+ index_location "middle" (default) places the index after the data symbols but before the redundancy symbols.
137
+ index_location "beginning" places the index before the data symbols.
138
+ index_location "end" places the index after the redundancy symbols. (not implemented)
139
+ lazy_outer (bool): If True, the outer code will be initialized lazily. Default: False.
150
140
  """
151
141
  if n_strands<=255:
152
142
  self.outer_alphabet_size_bytes = 1
@@ -159,6 +149,7 @@ class BaseNBlockCodec:
159
149
  max_strand_index = n_strands
160
150
  if max_strand_index<n_strands:
161
151
  raise ValueError("max_strand_index must be greater than or equal to n_strands.")
152
+ self.max_strand_index = max_strand_index
162
153
 
163
154
  if index_type not in {"binary","inner symbol","inner"}:
164
155
  raise ValueError("index_type must be 'binary' or 'inner symbol'.")
@@ -183,6 +174,7 @@ class BaseNBlockCodec:
183
174
  # make sure extra sequence numbers are not added to inner blocks. and or maybe add a
184
175
  # parameter for the number of inner blocks per strand.
185
176
  assert inner_n<inner_alphabet_size
177
+ self.inner_n = inner_n
186
178
 
187
179
  self.inner_coder = galois.ReedSolomon(n=inner_alphabet_size-1,d=inner_d,field=galois.GF(inner_alphabet_size))
188
180
  # in units of symbols (there are q=inner_alphabet_size possible symbols)
@@ -199,12 +191,12 @@ class BaseNBlockCodec:
199
191
  self.inner_k = k
200
192
  q = self.inner_coder.field.order
201
193
 
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
194
 
206
195
  #message length in bytes (without index)
207
196
  if self.index_type == "binary":
197
+ #number of bytes needed to store the index
198
+ self.index_bits = np.ceil(np.log2(max_strand_index)).astype(int)
199
+ logging.debug("bits per strand used for indexing: {}".format(self.index_bits))
208
200
  self.data_chunk_size = int(k*np.log2(q)-self.index_bits) // 8
209
201
  elif self.index_type == "inner":
210
202
  self.index_symbols = len(_int_to_baseN(max_strand_index,q))
@@ -222,7 +214,20 @@ class BaseNBlockCodec:
222
214
  logging.debug( "codec message size: {} bytes".format(self.block_capacity_bytes) )
223
215
 
224
216
  #init the outer coder
225
- self.outer_coder = reedsolo.RSCodec(n_redundant_strands, c_exp=8*self.outer_alphabet_size_bytes)
217
+ if self.outer_alphabet_size_bytes==2 and self.data_chunk_size%2!=0:
218
+ raise ValueError("data_chunk_size must be even for 16 bit outer code.")
219
+ #c=0 to match reedsolo library's default for code compatibility with prior versions of this codec.
220
+ # This may be changed in a future major version.
221
+
222
+ self.outer_d = n_redundant_strands+1
223
+ self.outer_field = galois.GF(2**(self.outer_alphabet_size_bytes*8))
224
+ if lazy_outer:
225
+ self.outer_coder_fast = None
226
+ else:
227
+ self.outer_coder_fast = galois.ReedSolomon(n=2**(self.outer_alphabet_size_bytes*8)-1,
228
+ d=self.outer_d,
229
+ c=0,
230
+ field=self.outer_field)
226
231
 
227
232
  def __compute_waste(self):
228
233
  k = self.inner_k
@@ -243,6 +248,77 @@ class BaseNBlockCodec:
243
248
  else:
244
249
  logging.debug("{} symbols wasted per strand".format( _wasted_symbols))
245
250
 
251
+ def generate_sector1(self, alphabets,vendor_id = "UNKNOWN", primer_len=40, file_meta_data=None):
252
+ #change to lazy imports when python 3.15 is common.
253
+ #these don't take particularly long to import but i don't want to bloat things more.
254
+ import sys
255
+ import uuid
256
+ import json
257
+ from collections import OrderedDict
258
+
259
+ alen = len(alphabets[0])
260
+ abases = len(alphabets[0][0])
261
+
262
+ for a in alphabets:
263
+ if len(a) != alen:
264
+ raise ValueError("All alphabets must be of the same length")
265
+ for s in a:
266
+ if len(s) != abases:
267
+ raise ValueError("All elements of alphabets must be the same number of bases")
268
+
269
+ files_nstrands = 0
270
+ if file_meta_data is not None:
271
+ for block_name, block_meta in file_meta_data.items():
272
+ strand_count_code_block = block_meta["blen"]//self.data_chunk_size+self.outer_d-1
273
+ strand_count = strand_count_code_block*len(block_meta["istart"])
274
+ files_nstrands+=strand_count
275
+
276
+ #encode as one string to save space.
277
+ alphabets = [" ".join(x) for x in alphabets]
278
+
279
+ sector1 = OrderedDict(sector01.get_sector_1_obj())
280
+ #reserve octets 4-5 for the number of strands in sector 1.
281
+ u = bytearray(uuid.uuid4().bytes)
282
+ u[6] = 8<<4
283
+ u[8] = (u[8] & 0b00111111) | 0b10000000
284
+ u[4] = 0
285
+ u[5] = 0
286
+ sector1["id"] = str(uuid.UUID(bytes=bytes(u) ))
287
+ sector1["codec"]["ver"] = sys.modules['.'.join(__name__.split('.')[:-1])].__version__
288
+ codec_params = sector1["codec"]["params"]
289
+ codec_params["alphs"] = alphabets
290
+ codec_params["innerd"] = self.inner_coder.d
291
+ codec_params["innern"] = self.inner_n
292
+ codec_params["nstrands"] = self.n_strands
293
+ codec_params["neccstrand"] = self.outer_d-1
294
+ codec_params["imax"] = self.max_strand_index
295
+ codec_params["itype"] = self.index_type
296
+ codec_params["ilocation"] = self.index_location
297
+ sector1["vendorid"] = vendor_id
298
+ seq_params = sector1["seq"]
299
+ osize = self.inner_n*abases+primer_len
300
+ seq_params["osizemin"] = osize
301
+ seq_params["osizemedian"] = osize
302
+ seq_params["osizemax"] = osize
303
+ seq_params["ocount"] = files_nstrands
304
+ #these two don't make sense
305
+ seq_params["orepmin"] = 1
306
+ seq_params["orepmax"] = 100
307
+
308
+ if file_meta_data is not None:
309
+ sector1["addl"] = {"fmeta": file_meta_data}
310
+
311
+ sector1.move_to_end("id",last=False) #guarentee this is in the first code block.
312
+ #add this in when file meta is present.
313
+ #s1_len = len(json.dumps(sector1,separators=(',', ':')).encode('utf-8'))
314
+ #s1_strands = s1_len//self.data_chunk_size + (1 if s1_len%self.data_chunk_size>0 else 0)
315
+ #s1_redundant_strands = 50*(s1_len//self.block_capacity_bytes + (1 if s1_len%self.block_capacity_bytes>0 else 0))
316
+ #s1_strands += s1_redundant_strands
317
+
318
+ #sector1["id"] = uid+f":{s1_strands:05d}"
319
+
320
+ return sector1
321
+
246
322
  def encode(self,data:bytes,index_start:int=0)->list[list[int]]:
247
323
  """ Encodes the given data using the outer and inner codes.
248
324
 
@@ -290,14 +366,21 @@ class BaseNBlockCodec:
290
366
  # ...
291
367
  # |<rrrrrrrrrrrrrrrrr self.data_chunk_size rrrrrrrrrrrrraaaRRRRRRRR>|
292
368
 
369
+ #lazy init outer code
370
+ if self.outer_coder_fast is None:
371
+ self.outer_coder_fast = galois.ReedSolomon(n=2**(self.outer_alphabet_size_bytes*8)-1,
372
+ d=self.outer_d,
373
+ c=0,
374
+ field=self.outer_field)
375
+
293
376
  #1. Reshape data
294
377
  logging.debug("applying outer code to {} bytes".format(len(data)))
295
378
  # reshape data so columns can be coded independently.
296
- if self.outer_coder.c_exp==8:
379
+ if self.outer_alphabet_size_bytes==1:
297
380
  data_np = np.frombuffer(data,dtype=np.uint8)
298
381
  assert len(data_np)%self.data_chunk_size == 0
299
382
  data_np = data_np.reshape(-1,self.data_chunk_size)
300
- elif self.outer_coder.c_exp==16:
383
+ elif self.outer_alphabet_size_bytes==2:
301
384
  data_np = np.frombuffer(data,dtype=np.uint16)
302
385
  if self.data_chunk_size%2!=0:
303
386
  raise ValueError("data_chunk_size must be even for 16 bit outer code.")
@@ -309,7 +392,8 @@ class BaseNBlockCodec:
309
392
  raise ValueError("Unsupported outer code byte size.")
310
393
 
311
394
  #2. apply the outer code (encode columns of data)
312
- data_enc_cols = np.array([self.outer_coder.encode(dc) for dc in data_np.transpose()],dtype=data_np.dtype)
395
+ data_enc_cols = np.array([self.outer_coder_fast.encode(dc) for dc in data_np.transpose()],dtype=data_np.dtype)
396
+
313
397
  data_enc_chunks = data_enc_cols.transpose()
314
398
 
315
399
  logging.debug("outer coded strands: {}".format(data_enc_chunks.shape[0]))
@@ -347,16 +431,17 @@ class BaseNBlockCodec:
347
431
 
348
432
  return ic_chunks
349
433
 
350
- def decode(self, data: list[list[int]], index_start: int = 0) -> Tuple[bytes,ArrayLike,ArrayLike,ArrayLike]:
434
+ def decode(self, data: list[list[int]], index_start: int = 0, n_strands: int|None = None) -> Tuple[bytes|None,ArrayLike,ArrayLike,ArrayLike]:
351
435
  """
352
436
  Decodes the given data.
353
437
 
354
438
  Args:
355
439
  data (list[list[int]]): The encoded data to be decoded.
356
440
  index_start (int, optional): The starting index for decoding. Defaults to 0.
441
+ n_strands (int, optional): The number of strands expected. Defaults to n_strands set in constructor.
357
442
 
358
443
  Returns:
359
- bytes: The decoded data.
444
+ bytes|None: The decoded data. Or None if decoding failed.
360
445
  outer erasures: list of erasures in outer code.
361
446
  outer errors: list of errors in outer code.
362
447
  inner errors: list of number of errors in inner code by strand index. -1 -> strand not present.
@@ -364,17 +449,31 @@ class BaseNBlockCodec:
364
449
  Raises:
365
450
  None
366
451
  """
452
+
453
+ #lazy init outer code
454
+ if self.outer_coder_fast is None:
455
+ self.outer_coder_fast = galois.ReedSolomon(n=2**(self.outer_alphabet_size_bytes*8)-1,
456
+ d=self.outer_d,
457
+ c=0,
458
+ field=self.outer_field)
459
+
460
+ if n_strands is None:
461
+ n_strands = self.n_strands
462
+
367
463
  # inner decode
368
464
  ## TODO: handle erasures in inner code.
369
465
 
370
466
  # 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
467
+ chunked_data:list[Tuple[galois.FieldArray,int]] = [self.inner_coder.decode(d,errors=True) for d in data if d != []] #type:ignore
372
468
 
373
469
 
374
470
  # handle outer decode
375
- ordered_chunks:list[bytes|None] = [None] * self.n_strands
376
- chunk_errors:list[int] = [-1] * self.n_strands
471
+ ordered_chunks:list[bytes|None] = [None] * n_strands
472
+ chunk_errors:list[int] = [-1] * n_strands
377
473
  for chunk in chunked_data:
474
+ #TODO: decide if its better to throw these out or keep them.
475
+ #if chunk[1]<0: #can't decode and the address may be bad so discard.
476
+ # continue
378
477
  if self.index_type == "binary":
379
478
  chunk_int = _baseN_to_int(chunk[0].tolist(), self.inner_coder.field.order)
380
479
  chunk_index = (chunk_int >> (self.data_chunk_size * 8)) - index_start
@@ -391,12 +490,16 @@ class BaseNBlockCodec:
391
490
  raise NotImplementedError("index_location 'end' is not implemented.")
392
491
  else:
393
492
  raise ValueError("Unknown index type: {}".format(self.index_type)) #should be impossible to get here.
394
- chunk_bytes:bytes = chunk_int.to_bytes(self.data_chunk_size, 'little')
493
+ #this can be to big sometimes. TODO handle more gracefully
395
494
  try:
396
- ordered_chunks[chunk_index] = chunk_bytes
397
- chunk_errors[chunk_index] = chunk[1]
398
- except IndexError:
399
- logging.info("strand index out of bounds:" + str(chunk_index))
495
+ chunk_bytes:bytes = chunk_int.to_bytes(self.data_chunk_size, 'little')
496
+ try:
497
+ ordered_chunks[chunk_index] = chunk_bytes
498
+ chunk_errors[chunk_index] = chunk[1]
499
+ except IndexError:
500
+ logging.info("strand index out of bounds:" + str(chunk_index))
501
+ except OverflowError:
502
+ logging.info("chunk overflow for strand index:" + str(chunk_index)+ "intval: "+ str(chunk_int))
400
503
 
401
504
  # identify erasures
402
505
  erasures = []
@@ -407,20 +510,32 @@ class BaseNBlockCodec:
407
510
  #we're all bytes now, but the checker can't tell that.
408
511
  ordered_chunks_bytes:list[bytes] = typing.cast(list[bytes],ordered_chunks)
409
512
 
410
- if self.outer_coder.c_exp==8:
513
+ # report erasures (lost strands) in outer code
514
+ logging.info("erasures (lost) chunks: {}".format(erasures))
515
+
516
+ if self.outer_alphabet_size_bytes==1:
411
517
  outer_dtype = np.uint8
412
- elif self.outer_coder.c_exp==16:
518
+ elif self.outer_alphabet_size_bytes==2:
413
519
  outer_dtype = np.uint16
414
520
  else:
415
521
  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
- data_decoded_results = [self.outer_coder.decode(dc, erase_pos=erasures) for dc in data_np_encoded]
418
- data_decoded_chunks = np.array([d[0] for d in data_decoded_results],dtype=outer_dtype).transpose()
419
- data_decoded = data_decoded_chunks.flatten().tobytes()
522
+ data_np_encoded = np.array([np.frombuffer(c, dtype=outer_dtype) for c in ordered_chunks_bytes]).transpose()
523
+
524
+ has_erasuers = len(erasures)>0
525
+ erasures_bool = np.zeros(data_np_encoded.shape[1],dtype=bool)
526
+ if has_erasuers:
527
+ erasures_bool[erasures] = True
528
+ data_decoded_results = [self.outer_coder_fast.decode(dc,errors=True,erasures=erasures_bool) for dc in data_np_encoded]
529
+ data_errata_col_pos = [] #galois doesn't report error positions
530
+ data_num_errors = [int(x[1]) for x in data_decoded_results]
531
+ if -1 in data_num_errors:
532
+ logging.error("Outer code decode failed on at least one column.")
533
+ return None, erasures, data_num_errors, chunk_errors
534
+ #raise ValueError(f"Fast mode outer code decode failed on at least one column. numerrors:{data_num_errors}")
535
+ logging.info("Found {} outer code column errors.".format(data_num_errors))
536
+ data_decoded_chunks = np.array([np.array(d[0]) for d in data_decoded_results],dtype=outer_dtype).transpose()
420
537
 
421
- # report erasures (lost strands) in outer code
422
- logging.info("erasures (lost) chunks: {}".format(erasures))
423
- data_errata_col_pos = [e[2] for e in data_decoded_results]
538
+ data_decoded = data_decoded_chunks.flatten().tobytes()
424
539
 
425
540
  errors = []
426
541
  for i, pos in enumerate(data_errata_col_pos):
@@ -434,20 +549,23 @@ class BaseNBlockCodec:
434
549
  if e[0] in erasures:
435
550
  pass
436
551
  else:
437
- logging.warning("Outer code errors found on strand {}, byte {}.".format(*e))
552
+ logging.info("Outer code errors found on strand {}, byte {}.".format(*e))
553
+
554
+ return data_decoded, erasures, data_num_errors, chunk_errors # pyright: ignore[reportPossiblyUnboundVariable]
438
555
 
439
- return data_decoded, erasures, errors, chunk_errors
440
556
 
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:
557
+ 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
558
  """
443
559
  Extracts the index from the given DNA strand.
444
560
 
445
561
  Args:
446
562
  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. Defaults to False.
449
- padding Literal["left","right"], optional): Assume any missing bases are left or right of index. Defaults to "right".
450
- Note padded sequences are unlikely to error correct properly.
563
+ error_check (bool|Literal["erasures"], optional): If True, the inner_codec will be used to
564
+ attempt to check for errors. if "erasures", the inner codec will check only if it encounters an N.
565
+ Defaults to False.
566
+ padding Literal["left","right"], optional): Assume any missing bases are left or right of index.
567
+ Defaults to "right".
568
+ Note: padded sequences are unlikely to error correct properly.
451
569
  words (list[str]): The primary set of base words used, in order, for encoding.
452
570
  alternate_words (list[str]): The alternate (aka synonymous) set of words used, in order, for encoding.
453
571
 
@@ -466,12 +584,30 @@ class BaseNBlockCodec:
466
584
  elif padding == "left":
467
585
  dna_s = "A"*num_missing + dna_s
468
586
 
587
+
588
+ if error_check == "erasures":
589
+ erasures = np.zeros(len(dna_s)//symbol_len,dtype=bool)
590
+ _temp = []
591
+ for i,c in enumerate(dna_s):
592
+ if c.upper() not in "ACGT":
593
+ erasures[i//symbol_len] = True
594
+ _temp.append("A")
595
+ else:
596
+ _temp.append(c)
597
+ has_erasures = bool(np.any(erasures))
598
+ if has_erasures: #don't bother if nothing has changed.
599
+ dna_s = "".join(_temp)
600
+ else:
601
+ has_erasures = False
602
+ erasures = None
603
+
604
+ ec = (has_erasures and error_check=="erasures") or (error_check==True)
469
605
  #This could fail if padding makes a symbol not in words or alternate_words.
470
606
  data = dna_to_bN([dna_s],words,alternate_words)[0]
471
607
 
472
- return self.extract_index(data,error_check)
608
+ return self.extract_index(data,ec,erasures)
473
609
 
474
- def extract_index(self, data: list[int], error_check: bool = False) -> int:
610
+ def extract_index(self, data: list[int], error_check: bool = False, erasures: np.ndarray|None = None) -> int:
475
611
  """
476
612
  Extracts the index from the given inner code sequence.
477
613
 
@@ -480,6 +616,7 @@ class BaseNBlockCodec:
480
616
  If a list[int] is given, it is assumed be be inner code symbols.
481
617
  error_check (bool, optional): If True, the inner_codec will be used to
482
618
  attempt to check for errors. Defaults to False.
619
+ erasures (np.ndarray|None, optional): A boolean array indicating which positions are erased. Defaults to None.
483
620
 
484
621
  Returns:
485
622
  int: The index. or -1 if error_check==True and unrecoverable error found.
@@ -487,7 +624,7 @@ class BaseNBlockCodec:
487
624
  if error_check == False:
488
625
  data = data[0:self.inner_k] #extract message
489
626
  else:
490
- data_gf, n_corrected = self.inner_coder.decode(data,errors=True)
627
+ data_gf, n_corrected = self.inner_coder.decode(data,errors=True,erasures=erasures) #type:ignore
491
628
  data = data_gf.tolist()
492
629
  if n_corrected < 0:
493
630
  return -1
@@ -549,7 +686,7 @@ def longest_binder(a:str,b:str):
549
686
 
550
687
 
551
688
 
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]]:
689
+ 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
690
  """
554
691
  TODO: rename to bN_to_DNA_optimize, and add test.
555
692
  file_data: 2d list of integral types (symbols)
@@ -810,3 +947,79 @@ def remove_bytes(data:bytes, insert:int|bytes, chunk_len:int, num_inserts:int) -
810
947
  del data[ins_index:ins_index + insert_len]
811
948
  return bytes(data)
812
949
 
950
+
951
+ ########################
952
+ # Sector 0 and 1 funcs #
953
+ ########################
954
+
955
+
956
+ def sector_1_codec():
957
+ """Returns a BaseNBlockCodec object with parameters suitable for encoding sector 1 of the SyNCODER format."""
958
+ return BaseNBlockCodec(inner_alphabet_size=32,
959
+ inner_d=5,
960
+ inner_n=70//3,
961
+ n_strands=255,
962
+ n_redundant_strands=50,
963
+ max_strand_index=2**(5*3)-1,#32,767 strands max.
964
+ index_type="inner")
965
+
966
+ 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]:
967
+ """Generates the sector 1 object for the SyNCODER format.
968
+
969
+ Args:
970
+ alphabets (list of list of str): A list of alphabets, where each alphabet is a list of strings representing the symbols in that alphabet.
971
+ vendor_id (str): A DNA string representing the vendor ID.
972
+ file_meta_data (dict): A dictionary containing metadata about the files being encoded.
973
+ primer_len (int, optional): The length of the primers to be used in sector 1. Defaults to 40.
974
+ 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.
975
+
976
+ Returns:
977
+ list[bytes]: A list of bytes representing the encoded sector 1 object.
978
+ """
979
+
980
+ assert len(vendor_id) == 35 #required by spec.
981
+ s1codec = sector_1_codec()
982
+ sector1 = codec.generate_sector1(alphabets,vendor_id,primer_len,file_meta_data)
983
+
984
+ if length>=0:
985
+ nstarnds_ashex = hex(length)[2:].rjust(4,'0')
986
+ id_parts = sector1["id"].split("-")
987
+ id_parts[1] = nstarnds_ashex
988
+ sector1["id"] = "-".join(id_parts)
989
+
990
+ s1json = json.dumps(sector1,separators=(',', ':')).encode('utf-8')
991
+
992
+ padding = s1codec.data_chunk_size - (len(s1json) % s1codec.data_chunk_size)
993
+ if padding != s1codec.data_chunk_size:
994
+ # Distribute padding bytes by inserting at most one space after commas
995
+ new_chars = bytearray(b" ") #put one space at the start.
996
+ rem = padding-1
997
+ for c in s1json:
998
+ new_chars.append(c)
999
+ if c == ',' and rem > 0:
1000
+ new_chars.append(ord(' ')) # add a space after the comma
1001
+ rem -= 1
1002
+ if rem > 0: #append any remaining spaces at the end (probably wont happen)
1003
+ new_chars.extend(b" " * rem)
1004
+ s1json_padded = bytes(new_chars)
1005
+ assert json.loads(s1json_padded) == sector1, "Padded JSON does not match original data"
1006
+ s1json = s1json_padded
1007
+
1008
+ encoded_s1 = []
1009
+ cbstart = 0
1010
+ istart = 0
1011
+ while cbstart < len(s1json):
1012
+ encoded_s1_block= s1codec.encode(s1json[cbstart:(cbstart+s1codec.block_capacity_bytes)],index_start=istart)
1013
+ istart+=len(encoded_s1_block)
1014
+ cbstart+=s1codec.block_capacity_bytes
1015
+ encoded_s1 += encoded_s1_block
1016
+
1017
+ #reoncode sector 1 with updated id.
1018
+ if length<0:
1019
+ #update sector1 id with number of strands in s1.
1020
+ return generate_sector1(codec,alphabets,vendor_id,file_meta_data,primer_len,length=len(encoded_s1))
1021
+
1022
+ #convert to base32 pad with a random base to meet the 70 base requirement for sector 1.
1023
+ 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)]
1024
+ s1DNA_with_primers = [sector01.s1fp_ext.encode("utf-8") + x + sector01.s1rp_rc_ext.encode("utf-8") for x in s1DNA]
1025
+ 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