dnakit 0.1.0.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. dnakit/__init__.py +108 -0
  2. dnakit/_version.py +5 -0
  3. dnakit/alignment/__init__.py +6 -0
  4. dnakit/alignment/pairwise.py +512 -0
  5. dnakit/alignment/results.py +188 -0
  6. dnakit/backends/__init__.py +80 -0
  7. dnakit/backends/external.py +517 -0
  8. dnakit/backends/registry.py +207 -0
  9. dnakit/batch.py +452 -0
  10. dnakit/cache/__init__.py +5 -0
  11. dnakit/cache/store.py +424 -0
  12. dnakit/chunking.py +920 -0
  13. dnakit/cli/__init__.py +5 -0
  14. dnakit/cli/__main__.py +6 -0
  15. dnakit/cli/app.py +814 -0
  16. dnakit/cli/workflow.py +94 -0
  17. dnakit/config/__init__.py +5 -0
  18. dnakit/config/loader.py +381 -0
  19. dnakit/core/__init__.py +80 -0
  20. dnakit/core/_json.py +206 -0
  21. dnakit/core/backend_info.py +70 -0
  22. dnakit/core/collection.py +163 -0
  23. dnakit/core/coordinates.py +295 -0
  24. dnakit/core/enums.py +95 -0
  25. dnakit/core/facade.py +719 -0
  26. dnakit/core/feature.py +90 -0
  27. dnakit/core/gap.py +82 -0
  28. dnakit/core/issues.py +62 -0
  29. dnakit/core/provenance.py +305 -0
  30. dnakit/core/record.py +161 -0
  31. dnakit/core/results.py +134 -0
  32. dnakit/core/sequence.py +327 -0
  33. dnakit/datasets/__init__.py +100 -0
  34. dnakit/datasets/_advanced_shared.py +198 -0
  35. dnakit/datasets/_metadata.py +33 -0
  36. dnakit/datasets/advanced_deduplicate.py +183 -0
  37. dnakit/datasets/advanced_split.py +302 -0
  38. dnakit/datasets/clustering.py +286 -0
  39. dnakit/datasets/config.py +531 -0
  40. dnakit/datasets/deduplicate.py +303 -0
  41. dnakit/datasets/evaluation.py +244 -0
  42. dnakit/datasets/filter.py +135 -0
  43. dnakit/datasets/neural_clustering.py +221 -0
  44. dnakit/datasets/results.py +520 -0
  45. dnakit/datasets/split.py +450 -0
  46. dnakit/descriptors/__init__.py +102 -0
  47. dnakit/descriptors/_dinucleotide.py +345 -0
  48. dnakit/descriptors/_shared.py +180 -0
  49. dnakit/descriptors/all.py +917 -0
  50. dnakit/descriptors/basic.py +193 -0
  51. dnakit/descriptors/codon.py +195 -0
  52. dnakit/descriptors/complexity.py +201 -0
  53. dnakit/descriptors/entropy.py +101 -0
  54. dnakit/descriptors/homopolymer.py +105 -0
  55. dnakit/descriptors/kmer.py +88 -0
  56. dnakit/descriptors/results.py +286 -0
  57. dnakit/descriptors/schema.py +414 -0
  58. dnakit/descriptors/window.py +203 -0
  59. dnakit/download/__init__.py +54 -0
  60. dnakit/download/catalogs.py +166 -0
  61. dnakit/download/ena.py +131 -0
  62. dnakit/download/encode.py +103 -0
  63. dnakit/download/files.py +469 -0
  64. dnakit/download/indexes.py +233 -0
  65. dnakit/download/metadata.py +164 -0
  66. dnakit/download/models.py +227 -0
  67. dnakit/download/ncbi.py +333 -0
  68. dnakit/download/sequence.py +133 -0
  69. dnakit/download/ucsc.py +99 -0
  70. dnakit/evaluation/__init__.py +81 -0
  71. dnakit/evaluation/_shared.py +288 -0
  72. dnakit/evaluation/collection.py +270 -0
  73. dnakit/evaluation/config.py +591 -0
  74. dnakit/evaluation/distribution.py +287 -0
  75. dnakit/evaluation/frechet.py +223 -0
  76. dnakit/evaluation/generative.py +381 -0
  77. dnakit/evaluation/reference.py +409 -0
  78. dnakit/evaluation/results.py +137 -0
  79. dnakit/evaluation/scorecard.py +211 -0
  80. dnakit/evaluation/sequence.py +403 -0
  81. dnakit/evaluation/synthesis.py +249 -0
  82. dnakit/exceptions.py +189 -0
  83. dnakit/fingerprints/__init__.py +109 -0
  84. dnakit/fingerprints/_shared.py +116 -0
  85. dnakit/fingerprints/advanced.py +829 -0
  86. dnakit/fingerprints/bit.py +229 -0
  87. dnakit/fingerprints/encoding.py +351 -0
  88. dnakit/fingerprints/kmer.py +170 -0
  89. dnakit/fingerprints/results.py +531 -0
  90. dnakit/fingerprints/sketch.py +246 -0
  91. dnakit/io/__init__.py +112 -0
  92. dnakit/io/_advanced_common.py +105 -0
  93. dnakit/io/_formats.py +994 -0
  94. dnakit/io/annotations.py +1179 -0
  95. dnakit/io/api.py +759 -0
  96. dnakit/io/config.py +315 -0
  97. dnakit/io/genbank.py +523 -0
  98. dnakit/io/indexed.py +1146 -0
  99. dnakit/io/metadata.py +452 -0
  100. dnakit/io/parquet.py +56 -0
  101. dnakit/io/results.py +131 -0
  102. dnakit/io/source.py +138 -0
  103. dnakit/io/tables.py +1237 -0
  104. dnakit/molbio/__init__.py +101 -0
  105. dnakit/molbio/_shared.py +266 -0
  106. dnakit/molbio/assembly.py +279 -0
  107. dnakit/molbio/crispr.py +181 -0
  108. dnakit/molbio/optimization.py +897 -0
  109. dnakit/molbio/primer3_design.py +667 -0
  110. dnakit/molbio/primers.py +899 -0
  111. dnakit/molbio/restriction.py +573 -0
  112. dnakit/molbio/results.py +583 -0
  113. dnakit/ops/__init__.py +588 -0
  114. dnakit/ops/_common.py +201 -0
  115. dnakit/ops/circular.py +193 -0
  116. dnakit/ops/concat.py +204 -0
  117. dnakit/ops/direction.py +27 -0
  118. dnakit/ops/edit.py +443 -0
  119. dnakit/ops/evolution.py +991 -0
  120. dnakit/ops/mutation.py +151 -0
  121. dnakit/ops/records.py +1012 -0
  122. dnakit/ops/translation.py +257 -0
  123. dnakit/patterns/__init__.py +81 -0
  124. dnakit/patterns/_shared.py +282 -0
  125. dnakit/patterns/coding.py +360 -0
  126. dnakit/patterns/crispr.py +337 -0
  127. dnakit/patterns/motif.py +721 -0
  128. dnakit/patterns/regions.py +341 -0
  129. dnakit/patterns/repeats.py +480 -0
  130. dnakit/patterns/restriction.py +281 -0
  131. dnakit/patterns/results.py +411 -0
  132. dnakit/py.typed +1 -0
  133. dnakit/references/__init__.py +27 -0
  134. dnakit/references/models.py +155 -0
  135. dnakit/references/ncbi.py +785 -0
  136. dnakit/representations/__init__.py +29 -0
  137. dnakit/representations/backends.py +928 -0
  138. dnakit/representations/checkpoints.py +356 -0
  139. dnakit/representations/extraction.py +220 -0
  140. dnakit/representations/models.py +395 -0
  141. dnakit/representations/results.py +79 -0
  142. dnakit/search/__init__.py +132 -0
  143. dnakit/search/_http.py +271 -0
  144. dnakit/search/_shared.py +108 -0
  145. dnakit/search/blast.py +516 -0
  146. dnakit/search/ena.py +245 -0
  147. dnakit/search/encode.py +102 -0
  148. dnakit/search/ensembl.py +874 -0
  149. dnakit/search/models.py +159 -0
  150. dnakit/search/ncbi.py +756 -0
  151. dnakit/search/ucsc.py +388 -0
  152. dnakit/secondary_structure/__init__.py +38 -0
  153. dnakit/secondary_structure/dotbracket.py +410 -0
  154. dnakit/secondary_structure/nupack.py +589 -0
  155. dnakit/secondary_structure/results.py +142 -0
  156. dnakit/similarity/__init__.py +125 -0
  157. dnakit/similarity/_shared.py +152 -0
  158. dnakit/similarity/approximate.py +258 -0
  159. dnakit/similarity/compare.py +162 -0
  160. dnakit/similarity/dashing.py +936 -0
  161. dnakit/similarity/distance.py +279 -0
  162. dnakit/similarity/index.py +365 -0
  163. dnakit/similarity/matrix.py +254 -0
  164. dnakit/similarity/results.py +676 -0
  165. dnakit/similarity/search.py +286 -0
  166. dnakit/similarity/sketch.py +96 -0
  167. dnakit/similarity/vector.py +662 -0
  168. dnakit/standardize/__init__.py +77 -0
  169. dnakit/standardize/_shared.py +100 -0
  170. dnakit/standardize/config.py +411 -0
  171. dnakit/standardize/gaps.py +1060 -0
  172. dnakit/standardize/normalize.py +565 -0
  173. dnakit/standardize/results.py +247 -0
  174. dnakit/standardize/validate.py +487 -0
  175. dnakit/structure3d/__init__.py +47 -0
  176. dnakit/structure3d/_shared.py +47 -0
  177. dnakit/structure3d/geometry.py +773 -0
  178. dnakit/structure3d/pdb.py +253 -0
  179. dnakit/structure3d/results.py +233 -0
  180. dnakit/structure3d/threedna.py +174 -0
  181. dnakit/thermodynamics/__init__.py +112 -0
  182. dnakit/thermodynamics/_shared.py +92 -0
  183. dnakit/thermodynamics/backends.py +653 -0
  184. dnakit/thermodynamics/calculations.py +837 -0
  185. dnakit/thermodynamics/config.py +175 -0
  186. dnakit/thermodynamics/equilibrium.py +340 -0
  187. dnakit/thermodynamics/optics.py +385 -0
  188. dnakit/thermodynamics/parameters.py +79 -0
  189. dnakit/thermodynamics/results.py +445 -0
  190. dnakit/visualization/__init__.py +43 -0
  191. dnakit/visualization/_svg.py +185 -0
  192. dnakit/visualization/advanced.py +314 -0
  193. dnakit/visualization/config.py +255 -0
  194. dnakit/visualization/export.py +349 -0
  195. dnakit/visualization/matrix.py +279 -0
  196. dnakit/visualization/report.py +431 -0
  197. dnakit/visualization/results.py +236 -0
  198. dnakit/visualization/sequence.py +459 -0
  199. dnakit/workflows/__init__.py +45 -0
  200. dnakit/workflows/manifest.py +365 -0
  201. dnakit/workflows/runner.py +1548 -0
  202. dnakit/workflows/schema.py +898 -0
  203. dnakit-0.1.0.dev0.dist-info/METADATA +126 -0
  204. dnakit-0.1.0.dev0.dist-info/RECORD +210 -0
  205. dnakit-0.1.0.dev0.dist-info/WHEEL +5 -0
  206. dnakit-0.1.0.dev0.dist-info/entry_points.txt +2 -0
  207. dnakit-0.1.0.dev0.dist-info/licenses/DISCLAIMER.md +26 -0
  208. dnakit-0.1.0.dev0.dist-info/licenses/LICENSE +21 -0
  209. dnakit-0.1.0.dev0.dist-info/licenses/THIRD_PARTY_NOTICES.md +73 -0
  210. dnakit-0.1.0.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,381 @@
1
+ """DNA adaptations of the MOSES Frag and SNN generative-set metrics."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from collections import Counter
7
+ from collections.abc import Iterable, Mapping
8
+ from typing import TypeVar, cast
9
+
10
+ from dnakit.core import DNARecord
11
+ from dnakit.core._json import FrozenDict
12
+ from dnakit.descriptors import kmer_statistics
13
+ from dnakit.exceptions import ConfigurationError
14
+ from dnakit.fingerprints import hashed_kmer_fingerprint
15
+
16
+ from ._shared import (
17
+ EvaluationInput,
18
+ enforce_pair_limit,
19
+ materialize_input,
20
+ pair_count,
21
+ record_for,
22
+ report,
23
+ require_nonempty,
24
+ )
25
+ from .config import EvaluationLimits, FragmentSimilarityConfig, SNNConfig
26
+ from .results import EvaluationEntry, EvaluationReport
27
+
28
+ _T = TypeVar("_T")
29
+
30
+
31
+ def _tracked(
32
+ items: tuple[_T, ...],
33
+ *,
34
+ enabled: bool,
35
+ description: str,
36
+ ) -> Iterable[_T]:
37
+ if not enabled:
38
+ return items
39
+ from rich.progress import track
40
+
41
+ return cast(Iterable[_T], track(items, description=description, total=len(items)))
42
+
43
+
44
+ def _materialize_collections(
45
+ generated: EvaluationInput,
46
+ reference: EvaluationInput,
47
+ *,
48
+ operation: str,
49
+ limits: EvaluationLimits,
50
+ ) -> tuple[tuple[DNARecord, ...], tuple[DNARecord, ...]]:
51
+ generated_items = materialize_input(generated, limits=limits)
52
+ reference_items = materialize_input(reference, limits=limits)
53
+ require_nonempty(generated_items, operation)
54
+ require_nonempty(reference_items, operation)
55
+ total_symbols = sum(item.sequence.symbol_length for item in generated_items + reference_items)
56
+ if total_symbols > limits.max_total_symbols:
57
+ raise ConfigurationError(
58
+ f"Combined {operation} inputs exceed max_total_symbols.",
59
+ code="EVALUATION_SYMBOL_LIMIT",
60
+ context={
61
+ "total_symbols": total_symbols,
62
+ "max_total_symbols": limits.max_total_symbols,
63
+ },
64
+ )
65
+ return (
66
+ tuple(record_for(item) for item in generated_items),
67
+ tuple(record_for(item) for item in reference_items),
68
+ )
69
+
70
+
71
+ def _fragment_counts(
72
+ records: tuple[DNARecord, ...],
73
+ *,
74
+ config: FragmentSimilarityConfig,
75
+ description: str,
76
+ initial_observations: int,
77
+ ) -> tuple[Counter[str], int, int]:
78
+ counts: Counter[str] = Counter()
79
+ observations = initial_observations
80
+ ignored_ambiguities = 0
81
+ for record in _tracked(
82
+ records,
83
+ enabled=config.show_progress,
84
+ description=description,
85
+ ):
86
+ result = kmer_statistics(
87
+ record,
88
+ config.k,
89
+ canonical=config.canonical,
90
+ ambiguity_policy=config.ambiguity_policy,
91
+ cross_gaps=False,
92
+ )
93
+ observations += result.denominator
94
+ if observations > config.max_kmer_observations:
95
+ raise ConfigurationError(
96
+ "Fragment similarity exceeds max_kmer_observations.",
97
+ code="FRAGMENT_KMER_LIMIT",
98
+ context={
99
+ "observation_count": observations,
100
+ "max_kmer_observations": config.max_kmer_observations,
101
+ },
102
+ )
103
+ counts.update(cast(Mapping[str, int], result.counts))
104
+ ignored_ambiguities += result.ignored_ambiguity_count
105
+ return counts, observations, ignored_ambiguities
106
+
107
+
108
+ def _cosine_counts(left: Counter[str], right: Counter[str]) -> tuple[float, float, float, float]:
109
+ left_squared = math.fsum(value * value for value in left.values())
110
+ right_squared = math.fsum(value * value for value in right.values())
111
+ if left_squared == 0 or right_squared == 0:
112
+ raise ConfigurationError(
113
+ "Fragment similarity requires at least one eligible k-mer in each collection.",
114
+ code="EMPTY_FRAGMENT_VECTOR",
115
+ context={"generated_empty": left_squared == 0, "reference_empty": right_squared == 0},
116
+ )
117
+ shared = set(left) & set(right)
118
+ dot = math.fsum(left[word] * right[word] for word in shared)
119
+ denominator = math.sqrt(left_squared * right_squared)
120
+ similarity = min(1.0, max(0.0, dot / denominator))
121
+ return similarity, dot, left_squared, right_squared
122
+
123
+
124
+ def evaluate_fragment_similarity(
125
+ generated: EvaluationInput,
126
+ reference: EvaluationInput,
127
+ *,
128
+ config: FragmentSimilarityConfig | None = None,
129
+ ) -> EvaluationReport:
130
+ """Compare exact DNA k-mer count distributions with cosine similarity."""
131
+
132
+ resolved = FragmentSimilarityConfig() if config is None else config
133
+ if not isinstance(resolved, FragmentSimilarityConfig):
134
+ raise ConfigurationError(
135
+ "config must be FragmentSimilarityConfig or None.",
136
+ code="INVALID_FRAGMENT_SIMILARITY_CONFIG",
137
+ )
138
+ generated_records, reference_records = _materialize_collections(
139
+ generated,
140
+ reference,
141
+ operation="fragment similarity",
142
+ limits=resolved.limits,
143
+ )
144
+ generated_counts, generated_observations, generated_ignored = _fragment_counts(
145
+ generated_records,
146
+ config=resolved,
147
+ description="Frag generated",
148
+ initial_observations=0,
149
+ )
150
+ reference_counts, total_observations, reference_ignored = _fragment_counts(
151
+ reference_records,
152
+ config=resolved,
153
+ description="Frag reference",
154
+ initial_observations=generated_observations,
155
+ )
156
+ similarity, dot, generated_squared, reference_squared = _cosine_counts(
157
+ generated_counts,
158
+ reference_counts,
159
+ )
160
+ shared_fragments = len(set(generated_counts) & set(reference_counts))
161
+ union_fragments = len(set(generated_counts) | set(reference_counts))
162
+ return report(
163
+ name="fragment_similarity",
164
+ method="cosine-similarity-of-exact-dna-kmer-counts",
165
+ version="eval-frag-dna-v1",
166
+ parameters={
167
+ "formula": "sum_f(c_G(f)*c_R(f)) / sqrt(sum_f(c_G(f)^2)*sum_f(c_R(f)^2))",
168
+ "molecular_analog": "MOSES Frag over BRICS fragment count distributions",
169
+ "dna_fragment_definition": "overlapping fixed-length A/C/G/T k-mers",
170
+ "k": resolved.k,
171
+ "canonical": resolved.canonical,
172
+ "overlapping": True,
173
+ "ambiguity_policy": resolved.ambiguity_policy,
174
+ "gap_policy": "k-mers never cross explicit Gap objects",
175
+ "topology_policy": "linearized at stored origin; no circular-origin wrap",
176
+ "higher_is_better": True,
177
+ "bounds": (0.0, 1.0),
178
+ "max_kmer_observations": resolved.max_kmer_observations,
179
+ "show_progress": resolved.show_progress,
180
+ "limits": resolved.limits,
181
+ "inference": (
182
+ "DNA k-mer adaptation; not BRICS Frag and not an experimentally "
183
+ "validated DNA quality score"
184
+ ),
185
+ },
186
+ metrics={
187
+ "score": similarity,
188
+ "frag": similarity,
189
+ "fragment_similarity": similarity,
190
+ "dot_product": dot,
191
+ "generated_squared_norm": generated_squared,
192
+ "reference_squared_norm": reference_squared,
193
+ "generated_count": len(generated_records),
194
+ "reference_count": len(reference_records),
195
+ "generated_fragment_observations": generated_observations,
196
+ "reference_fragment_observations": total_observations - generated_observations,
197
+ "generated_unique_fragments": len(generated_counts),
198
+ "reference_unique_fragments": len(reference_counts),
199
+ "shared_unique_fragments": shared_fragments,
200
+ "union_unique_fragments": union_fragments,
201
+ "generated_ignored_ambiguity_count": generated_ignored,
202
+ "reference_ignored_ambiguity_count": reference_ignored,
203
+ },
204
+ )
205
+
206
+
207
+ def _bit_fingerprints(
208
+ records: tuple[DNARecord, ...],
209
+ *,
210
+ config: SNNConfig,
211
+ description: str,
212
+ ) -> tuple[tuple[frozenset[str], ...], tuple[int, ...], int]:
213
+ fingerprints: list[frozenset[str]] = []
214
+ observation_counts: list[int] = []
215
+ ignored_ambiguities = 0
216
+ for record in _tracked(
217
+ records,
218
+ enabled=config.show_progress,
219
+ description=description,
220
+ ):
221
+ fingerprint = hashed_kmer_fingerprint(
222
+ record,
223
+ k=config.k,
224
+ n_bits=config.n_bits,
225
+ canonical=config.canonical,
226
+ seed=config.seed,
227
+ representation="sparse",
228
+ ambiguity_policy=config.ambiguity_policy,
229
+ cross_gaps=False,
230
+ )
231
+ fingerprints.append(frozenset(fingerprint.sparse_values()))
232
+ observation_counts.append(fingerprint.observation_count)
233
+ ignored_ambiguities += fingerprint.ignored_ambiguity_count
234
+ return tuple(fingerprints), tuple(observation_counts), ignored_ambiguities
235
+
236
+
237
+ def _binary_tanimoto(left: frozenset[str], right: frozenset[str]) -> float:
238
+ union_size = len(left | right)
239
+ if union_size == 0:
240
+ return 1.0
241
+ return len(left & right) / union_size
242
+
243
+
244
+ def evaluate_snn(
245
+ generated: EvaluationInput,
246
+ reference: EvaluationInput,
247
+ *,
248
+ config: SNNConfig | None = None,
249
+ ) -> EvaluationReport:
250
+ """Average each generated sequence's nearest-reference Tanimoto similarity."""
251
+
252
+ resolved = SNNConfig() if config is None else config
253
+ if not isinstance(resolved, SNNConfig):
254
+ raise ConfigurationError(
255
+ "config must be SNNConfig or None.",
256
+ code="INVALID_SNN_CONFIG",
257
+ )
258
+ generated_records, reference_records = _materialize_collections(
259
+ generated,
260
+ reference,
261
+ operation="SNN",
262
+ limits=resolved.limits,
263
+ )
264
+ comparisons = pair_count(len(generated_records), len(reference_records))
265
+ enforce_pair_limit(comparisons, resolved.limits)
266
+ fingerprint_elements = (len(generated_records) + len(reference_records)) * resolved.n_bits
267
+ if fingerprint_elements > resolved.max_fingerprint_elements:
268
+ raise ConfigurationError(
269
+ "SNN exceeds max_fingerprint_elements.",
270
+ code="SNN_FINGERPRINT_LIMIT",
271
+ context={
272
+ "fingerprint_elements": fingerprint_elements,
273
+ "max_fingerprint_elements": resolved.max_fingerprint_elements,
274
+ },
275
+ )
276
+ generated_fingerprints, generated_observations, generated_ignored = _bit_fingerprints(
277
+ generated_records,
278
+ config=resolved,
279
+ description="SNN generated fingerprints",
280
+ )
281
+ reference_fingerprints, reference_observations, reference_ignored = _bit_fingerprints(
282
+ reference_records,
283
+ config=resolved,
284
+ description="SNN reference fingerprints",
285
+ )
286
+
287
+ entries: list[EvaluationEntry] = []
288
+ nearest_values: list[float] = []
289
+ generated_pairs = tuple(zip(generated_records, generated_fingerprints, strict=True))
290
+ for generated_index, (record, fingerprint) in enumerate(
291
+ _tracked(
292
+ generated_pairs,
293
+ enabled=resolved.show_progress,
294
+ description="SNN nearest neighbors",
295
+ )
296
+ ):
297
+ best_index = 0
298
+ best_similarity = -1.0
299
+ for reference_index, reference_fingerprint in enumerate(reference_fingerprints):
300
+ similarity = _binary_tanimoto(fingerprint, reference_fingerprint)
301
+ if similarity > best_similarity:
302
+ best_index = reference_index
303
+ best_similarity = similarity
304
+ nearest_values.append(best_similarity)
305
+ nearest_fingerprint = reference_fingerprints[best_index]
306
+ shared_bit_count = len(fingerprint & nearest_fingerprint)
307
+ union_bit_count = len(fingerprint | nearest_fingerprint)
308
+ entries.append(
309
+ EvaluationEntry(
310
+ record.id,
311
+ generated_index,
312
+ FrozenDict(
313
+ {
314
+ "nearest_reference_id": reference_records[best_index].id,
315
+ "nearest_reference_index": best_index,
316
+ "nearest_similarity": best_similarity,
317
+ "generated_set_bit_count": len(fingerprint),
318
+ "nearest_reference_set_bit_count": len(nearest_fingerprint),
319
+ "nearest_shared_bit_count": shared_bit_count,
320
+ "nearest_union_bit_count": union_bit_count,
321
+ }
322
+ ),
323
+ )
324
+ )
325
+ snn = math.fsum(nearest_values) / len(nearest_values)
326
+ return report(
327
+ name="similarity_to_nearest_neighbor",
328
+ method="mean-nearest-binary-tanimoto-over-hashed-dna-kmer-fingerprints",
329
+ version="eval-snn-dna-v1",
330
+ parameters={
331
+ "formula": "mean_g max_r Tanimoto(fp(g), fp(r))",
332
+ "molecular_analog": (
333
+ "MOSES SNN over radius-2 Morgan fingerprints (1024 bits in the paper)"
334
+ ),
335
+ "dna_fingerprint": "SHA-256 hashed binary k-mer fingerprint",
336
+ "k": resolved.k,
337
+ "n_bits": resolved.n_bits,
338
+ "canonical": resolved.canonical,
339
+ "overlapping": True,
340
+ "seed": resolved.seed,
341
+ "ambiguity_policy": resolved.ambiguity_policy,
342
+ "gap_policy": "fingerprint k-mers never cross explicit Gap objects",
343
+ "topology_policy": "linearized at stored origin; no circular-origin wrap",
344
+ "zero_vector_policy": "two empty fingerprints are identical; one empty is dissimilar",
345
+ "tie_break": "reference-input-index-ascending",
346
+ "higher_is_better": True,
347
+ "bounds": (0.0, 1.0),
348
+ "max_fingerprint_elements": resolved.max_fingerprint_elements,
349
+ "show_progress": resolved.show_progress,
350
+ "limits": resolved.limits,
351
+ "inference": (
352
+ "DNA k-mer fingerprint adaptation; not Morgan-fingerprint SNN and not an "
353
+ "experimentally validated DNA quality score"
354
+ ),
355
+ },
356
+ metrics={
357
+ "score": snn,
358
+ "snn": snn,
359
+ "mean_nearest_similarity": snn,
360
+ "minimum_nearest_similarity": min(nearest_values),
361
+ "maximum_nearest_similarity": max(nearest_values),
362
+ "generated_count": len(generated_records),
363
+ "reference_count": len(reference_records),
364
+ "pairwise_comparison_count": comparisons,
365
+ "fingerprint_elements": fingerprint_elements,
366
+ "generated_zero_fingerprint_count": sum(
367
+ not fingerprint for fingerprint in generated_fingerprints
368
+ ),
369
+ "reference_zero_fingerprint_count": sum(
370
+ not fingerprint for fingerprint in reference_fingerprints
371
+ ),
372
+ "generated_kmer_observation_count": sum(generated_observations),
373
+ "reference_kmer_observation_count": sum(reference_observations),
374
+ "generated_ignored_ambiguity_count": generated_ignored,
375
+ "reference_ignored_ambiguity_count": reference_ignored,
376
+ },
377
+ entries=entries,
378
+ )
379
+
380
+
381
+ __all__ = ["evaluate_fragment_similarity", "evaluate_snn"]