evolutionary-stability-optimizer 0.1.0__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.
Files changed (26) hide show
  1. evolutionary_stability_optimizer-0.1.0/LICENSE +21 -0
  2. evolutionary_stability_optimizer-0.1.0/PKG-INFO +501 -0
  3. evolutionary_stability_optimizer-0.1.0/README.md +468 -0
  4. evolutionary_stability_optimizer-0.1.0/eso/__init__.py +11 -0
  5. evolutionary_stability_optimizer-0.1.0/eso/cli.py +111 -0
  6. evolutionary_stability_optimizer-0.1.0/eso/codon_usage.py +96 -0
  7. evolutionary_stability_optimizer-0.1.0/eso/constraints.py +179 -0
  8. evolutionary_stability_optimizer-0.1.0/eso/custom_score.py +170 -0
  9. evolutionary_stability_optimizer-0.1.0/eso/data/__init__.py +0 -0
  10. evolutionary_stability_optimizer-0.1.0/eso/data/human-antibody-heavy-chain-codon-frequencies.csv +62 -0
  11. evolutionary_stability_optimizer-0.1.0/eso/data/human-antibody-light-chain-codon-frequencies.csv +62 -0
  12. evolutionary_stability_optimizer-0.1.0/eso/detection/__init__.py +0 -0
  13. evolutionary_stability_optimizer-0.1.0/eso/detection/_overlap.py +112 -0
  14. evolutionary_stability_optimizer-0.1.0/eso/detection/common_motifs.py +81 -0
  15. evolutionary_stability_optimizer-0.1.0/eso/detection/dispatch.py +185 -0
  16. evolutionary_stability_optimizer-0.1.0/eso/detection/methylation.py +96 -0
  17. evolutionary_stability_optimizer-0.1.0/eso/detection/motif_utils.py +77 -0
  18. evolutionary_stability_optimizer-0.1.0/eso/detection/recombination.py +329 -0
  19. evolutionary_stability_optimizer-0.1.0/eso/detection/slippage.py +249 -0
  20. evolutionary_stability_optimizer-0.1.0/eso/detection/staubility_variant.py +284 -0
  21. evolutionary_stability_optimizer-0.1.0/eso/io_utils.py +312 -0
  22. evolutionary_stability_optimizer-0.1.0/eso/optimize.py +266 -0
  23. evolutionary_stability_optimizer-0.1.0/eso/pipeline.py +322 -0
  24. evolutionary_stability_optimizer-0.1.0/eso/report.py +57 -0
  25. evolutionary_stability_optimizer-0.1.0/eso/sequence_utils.py +41 -0
  26. evolutionary_stability_optimizer-0.1.0/pyproject.toml +58 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Itamar Menuhin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,501 @@
1
+ Metadata-Version: 2.4
2
+ Name: evolutionary-stability-optimizer
3
+ Version: 0.1.0
4
+ Summary: Evolutionary Stability Optimizer (ESO) - detects and removes hypermutable sites (recombination, slippage, methylation hotspots) from engineered DNA sequences while preserving translation and codon usage.
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Keywords: synthetic-biology,dna,codon-optimization,genetic-stability,bioinformatics
8
+ Author: Itamar Menuhin
9
+ Requires-Python: >=3.11,<4.0
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
19
+ Provides-Extra: docx-report
20
+ Requires-Dist: biopython (>=1.87,<2.0)
21
+ Requires-Dist: dnachisel (>=3.2.16,<4.0.0)
22
+ Requires-Dist: numpy (>=1.26,<3.0)
23
+ Requires-Dist: pandas (>=3.0.3,<4.0.0)
24
+ Requires-Dist: python-Levenshtein (>=0.27.3,<1.0.0)
25
+ Requires-Dist: python-docx (>=1.1.0,<2.0.0) ; extra == "docx-report"
26
+ Requires-Dist: scikit-learn (>=1.9.0.0,<2.0.0.0)
27
+ Project-URL: Bug Tracker, https://github.com/itamar-menuhin/evolutionary-stability-optimizer/issues
28
+ Project-URL: Documentation, https://github.com/itamar-menuhin/evolutionary-stability-optimizer#readme
29
+ Project-URL: Homepage, https://github.com/itamar-menuhin/evolutionary-stability-optimizer
30
+ Project-URL: Repository, https://github.com/itamar-menuhin/evolutionary-stability-optimizer
31
+ Description-Content-Type: text/markdown
32
+
33
+ # ESO - Evolutionary Stability Optimizer
34
+
35
+ [![Tests](https://github.com/itamar-menuhin/evolutionary-stability-optimizer/actions/workflows/tests.yml/badge.svg)](https://github.com/itamar-menuhin/evolutionary-stability-optimizer/actions/workflows/tests.yml)
36
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
37
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue.svg)](pyproject.toml)
38
+
39
+ ESO detects hypermutable sites in engineered DNA sequences and optimizes them away with
40
+ [DNAChisel](https://github.com/Edinburgh-Genome-Foundry/DNAChisel), while preserving the
41
+ amino-acid translation and optimizing for host codon usage. This implementation follows
42
+ the approach introduced in Menuhin-Gruman et al. (2022, *ACS Synthetic Biology*) - see
43
+ [Citation](#citation) below.
44
+
45
+ Genes built from repetitive or duplicated DNA elements (a side effect of standard codon
46
+ optimization, which tends to reuse the same "best" codon repeatedly) are prone to mutate
47
+ away during propagation in a host organism, through mechanisms like replication slippage
48
+ and recombination-mediated deletion. ESO detects these hotspots and asks DNAChisel to
49
+ route around them while it optimizes, using the empirical mutation-rate model from the
50
+ [EFM Calculator](https://doi.org/10.1021/acssynbio.5b00068) (Jack et al., 2015, ACS
51
+ Synthetic Biology).
52
+
53
+ ## Quickstart
54
+
55
+ **1. Requirements**: Python 3.11 or newer. Check what you have installed:
56
+
57
+ ```bash
58
+ python --version
59
+ ```
60
+
61
+ If that prints something below `Python 3.11`, or fails with "command not found"/"not
62
+ recognized", install a current version from [python.org/downloads](https://www.python.org/downloads/)
63
+ first (the installer's default settings are fine) before continuing.
64
+
65
+ **2. Install ESO.** In a terminal, `cd` into this folder (the one this README is in), then:
66
+
67
+ ```bash
68
+ pip install .
69
+ ```
70
+
71
+ This can take a minute or two the first time. If it ends with a line that doesn't contain
72
+ the word `error`, it worked - skip to step 3. If you do see an error, check
73
+ [Troubleshooting](#troubleshooting) below before asking for help.
74
+
75
+ **3. Try it on a real sequence.** Put a FASTA file (a plain text file starting with a `>`
76
+ line, then the DNA sequence) in a folder by itself, e.g. `my_sequences/gene.fasta`
77
+ containing:
78
+
79
+ ```
80
+ >my_gene
81
+ ATGGCTGCTGCTGCTGCTGCTGCTGCTGCTGCTGCTGCTGCTGCTGCTGCTTAA
82
+ ```
83
+
84
+ Then run:
85
+
86
+ ```bash
87
+ eso-optimize --input-folder my_sequences --output-path my_results
88
+ ```
89
+
90
+ **4. Check the result.** Look inside `my_results/gene/` - `final_sequence.txt` has the
91
+ optimized sequence, `recombination_sites.csv`/`slippage_sites.csv` list what was detected
92
+ and fixed. If the very last line printed to the terminal was `Success!`, it worked.
93
+
94
+ That's the whole loop. Everything below this point is reference material for going
95
+ further (different host organisms, custom scoring, locking regions from editing, and so
96
+ on) - not required to get a first result.
97
+
98
+ If you'd rather be walked through setup interactively instead of reading this file, try the
99
+ [ESO Onboarding Assistant](https://chatgpt.com/g/g-6a60d7b081108191a8bc208b89958267-eso-onboarding-assistant),
100
+ a ChatGPT assistant configured specifically for onboarding onto this tool (see
101
+ [`docs/custom-gpt-setup.md`](docs/custom-gpt-setup.md) for its configuration).
102
+
103
+ ## Troubleshooting
104
+
105
+ - **`'eso-optimize' is not recognized` / `command not found: eso-optimize`** (common on
106
+ Windows right after a fresh install): the command was installed, but its folder isn't on
107
+ your terminal's PATH yet. Use this instead - it always works, no PATH needed:
108
+
109
+ ```bash
110
+ python -m eso.cli --input-folder my_sequences --output-path my_results
111
+ ```
112
+
113
+ (Every `eso-optimize ...` example in this README works identically as
114
+ `python -m eso.cli ...`.)
115
+ - **An error mentioning `Microsoft Visual Studio`, `CMake`, or "failed building wheel"
116
+ during `pip install .`**: this means `pip` tried to compile a dependency from source
117
+ instead of using a prebuilt version - almost always fixed by upgrading pip first
118
+ (`python -m pip install --upgrade pip`) and trying the install again, since an older
119
+ `pip` can miss prebuilt wheels that a newer one finds.
120
+ - **`ModuleNotFoundError` or `ImportError` right after install**: double check you're
121
+ running `python`/`eso-optimize` from the *same* Python installation you ran
122
+ `pip install .` with. If you're not sure, run
123
+ `python -m pip show eso` - if that fails, you installed into a different Python than the
124
+ one you're now running.
125
+ - **"No such file or directory" for your input folder**: `--input-folder` is relative to
126
+ wherever your terminal's current directory is - either `cd` there first, or use a full
127
+ path (e.g. `C:\Users\you\my_sequences` or `/Users/you/my_sequences`).
128
+ - **Nothing in the output folder / an empty `results` list**: ESO looks for files ending
129
+ in `.fasta`/`.fna`/`.ffn`/`.faa`/`.frn`/`.fa`/`.gb`/`.gbk`/`.genbank` (optionally
130
+ gzipped), directly inside `--input-folder` or one level under it - check your file's
131
+ extension matches one of those.
132
+ - Anything else: the error messages this tool prints are meant to be read directly and
133
+ acted on (not just a Python traceback to decode) - if one isn't clear, that's a bug in
134
+ the tool itself, worth reporting rather than working around.
135
+
136
+ ## What it detects
137
+
138
+ - **Replication slippage** - short tandem repeats (a base unit of length 1-15 repeated
139
+ 3+ times, or a single nucleotide repeated 4+ times) that polymerase can skip or
140
+ duplicate during replication.
141
+ - **Recombination-mediated deletion (RMD)** - pairs of near-identical sites (16+ nt,
142
+ within Levenshtein distance 1 of each other) that homologous recombination can delete
143
+ between.
144
+ - **Methylation motifs** - sequence motifs recognized by host methylation machinery,
145
+ which can trigger repair-associated mutation. Given as a PSSM/MEME file, a bundled
146
+ common motif (E. coli's Dam/Dcm systems), or your own IUPAC consensus string
147
+ (e.g. `"GATC"`) - no file needed for either of the latter two.
148
+
149
+ Two independently-developed implementations of the recombination/slippage detectors are
150
+ included (`eso.detection.recombination`/`eso.detection.slippage`, and
151
+ `eso.detection.staubility_variant`) - they haven't yet been reconciled into a single
152
+ canonical algorithm. Both are routed through `eso.detection.dispatch`
153
+ (`find_recombination_sites(seq, num_sites, mode="thorough" | "fast")` and
154
+ `find_slippage_sites(seq, num_sites, mode="default" | "fast")`), also exposed via
155
+ `eso.pipeline.main(..., recombination_mode=..., slippage_mode=...)` /
156
+ `eso-optimize --recombination-mode --slippage-mode`. Both of these collapse overlapping
157
+ candidates down to one representative per distinct site - a human-facing report/count
158
+ view, and what `--num-sites`/`num_sites` limits. `eso.pipeline.main`/`eso-optimize` always
159
+ build correction constraints from every detected candidate regardless (see "Using ESO as
160
+ a library" below if you're calling detection yourself, and
161
+ `docs/detector-comparisons.md`'s coverage-gap entry for why). See
162
+ [`docs/detector-comparisons.md`](docs/detector-comparisons.md) for the tradeoffs,
163
+ benchmarks, and bugs found while comparing them - recombination's two modes trade off
164
+ sensitivity for speed, but slippage's are fully equivalent in what they detect (a pure
165
+ speed choice). Methylation motif detection only has one implementation
166
+ (`eso.detection.methylation`) - a second was built and compared, but deleted after it
167
+ turned out to disagree in accuracy, not just speed (see the same doc for the writeup).
168
+
169
+ **Not included**: STABLES also carried a junction-linker hotspot checker
170
+ (`linker_suspect_utils.py`), applying the same slippage/recombination-detection idea to a
171
+ different, narrower problem - checking whether joining a target gene to a host sequence
172
+ via a linker creates a *new* hotspot at the junction. It was ported into an earlier version
173
+ of this repo (`eso.detection.junction_linker`) but removed: its detection thresholds (a
174
+ 4-repeat homopolymer check, a 12-mer recombination check) don't match the calibrated -9
175
+ filter this codebase's other detectors use, and tracing them back into the original
176
+ STABLES `select_fusion_linkers.py` pipeline showed they act as a near-absolute veto in
177
+ STABLES' own linker-selection algorithm - deliberate, STABLES-specific conservatism for
178
+ screening many cheap, redesignable linker candidates, not a general-purpose hotspot
179
+ detector. That's a different problem from ESO's (evaluating one already-chosen sequence),
180
+ so it was judged out of scope for this library rather than reconciled with the other
181
+ detectors' thresholds.
182
+
183
+ ## Install
184
+
185
+ ```bash
186
+ poetry install
187
+ # or: pip install -e .
188
+ ```
189
+
190
+ Word-document diff reports (`eso.report`) need the optional `docx-report` extra:
191
+
192
+ ```bash
193
+ poetry install -E docx-report
194
+ ```
195
+
196
+ ## Usage
197
+
198
+ ```python
199
+ from eso import main
200
+
201
+ message, results = main(
202
+ input_folder="path/to/fasta_files",
203
+ output_path="path/to/output",
204
+ optimize=True,
205
+ mini_gc=0.3,
206
+ maxi_gc=0.7,
207
+ method="use_best_codon",
208
+ organism_name="e_coli", # or a TaxID, or a bundled table (see eso.codon_usage)
209
+ )
210
+ ```
211
+
212
+ Or from the command line:
213
+
214
+ ```bash
215
+ eso-optimize --input-folder path/to/fasta_files --output-path path/to/output --organism-name e_coli
216
+ ```
217
+
218
+ For each FASTA/GenBank file found in `input_folder`, this writes to `output_path/<file_stem>/`:
219
+
220
+ - `final_sequence.txt` - the optimized sequence, plus CAI-before/after and edit-count stats.
221
+ - `recombination_sites.csv` / `slippage_sites.csv` / `motif_sites.csv` - detected hotspots,
222
+ one row per distinct site (a human-facing report, limited by `--num-sites` if given).
223
+ - `recombination_sites_corrected.csv` / `slippage_sites_corrected.csv` (only written when
224
+ `optimize` is on) - every candidate that actually received a correction constraint during
225
+ optimization. This can list more rows than the report CSVs above - collapsing several
226
+ overlapping candidates down to one representative per distinct site is the right call for
227
+ a report, but optimization still needs to correct every one of them individually to avoid
228
+ silently leaving part of a real hotspot unconstrained (see
229
+ [`docs/detector-comparisons.md`](docs/detector-comparisons.md)'s overlap-collapse
230
+ coverage-gap entry). Check this file, not the report CSVs, if you're trying to account
231
+ for every edit in `final_sequence.txt`.
232
+ - `sequence_comparison.docx` - a diff view of original vs. optimized sequence (if the
233
+ `docx-report` extra is installed).
234
+
235
+ `organism_name` accepts anything supported by
236
+ [python-codon-tables](https://github.com/Edinburgh-Genome-Foundry/python_codon_tables)
237
+ (a species name or NCBI TaxID), or one of the bundled custom tables in
238
+ `eso.codon_usage.CODON_USAGE_TABLES` (`C1`, `kompas`, `human_antibody_heavy_chain`,
239
+ `human_antibody_light_chain`) for hosts not in that database.
240
+
241
+ See [`examples/antibody_optimization`](examples/antibody_optimization) for a complete
242
+ worked example (human antibody heavy/light chain optimization).
243
+
244
+ ## Using ESO as a library, instead of files
245
+
246
+ `main()` (above) is file-in, file-out - convenient for a one-off CLI run, but if you
247
+ already have sequences in memory as part of your own code (e.g. generated, fetched from a
248
+ database, or produced by an earlier step in your own pipeline), you don't need to write
249
+ them to disk first. `eso.optimize.optimization_engine` (also importable as
250
+ `eso.optimization_engine`) takes a plain DNA string and returns a plain DNA string - no
251
+ files involved:
252
+
253
+ ```python
254
+ from eso import optimization_engine, suspect_site_extractor
255
+
256
+ seq = "ATG" + "GCT" * 15 + "TAA" # any DNA string you already have
257
+
258
+ # 1. detect hotspots (skip this step and the dataframes below entirely if you only
259
+ # want codon/GC optimization, with no hotspot avoidance)
260
+ sites = suspect_site_extractor(seq, compute_motifs=False, num_sites=50)
261
+
262
+ # 2. optimize, avoiding what was detected - use the _raw dataframes, NOT
263
+ # sites["df_recombination"]/sites["df_slippage"] (those are collapsed to one
264
+ # representative per distinct site, for a human-facing report/count - using
265
+ # them here can silently leave part of a real, only-partially-overlapping
266
+ # hotspot with no correction at all; see docs/detector-comparisons.md)
267
+ final_seq, objectives_summary, num_edits = optimization_engine(
268
+ seq,
269
+ organism_name="e_coli",
270
+ df_recombination=sites["df_recombination_raw"],
271
+ df_slippage=sites["df_slippage_raw"],
272
+ )
273
+ ```
274
+
275
+ `final_seq` is a plain `str` you can feed straight back into whatever your own code does
276
+ next (write it out yourself, pass it to another function, etc.) - nothing here touches
277
+ the filesystem. `suspect_site_extractor` (also importable as
278
+ `eso.suspect_site_extractor`) is the same detection step `main()` runs internally; call
279
+ it on its own if you only want the hotspot dataframes, with no optimization at all.
280
+ `num_sites` only limits `df_recombination`/`df_slippage` (the reported view) - the `_raw`
281
+ dataframes always include every detected candidate, regardless of `num_sites`.
282
+
283
+ This composes directly with a custom scoring function - just pass `custom_score_fn=...`
284
+ to `optimization_engine` instead of `organism_name`:
285
+
286
+ ```python
287
+ final_seq, _, num_edits = optimization_engine(
288
+ seq,
289
+ custom_score_fn=my_model.predict, # any function: whole seq (str) -> a number, higher = better
290
+ df_recombination=sites["df_recombination_raw"],
291
+ df_slippage=sites["df_slippage_raw"],
292
+ )
293
+ ```
294
+
295
+ See `optimization_engine`'s docstring (`eso/optimize.py`) for every parameter
296
+ (`mini_gc`/`maxi_gc`, `orf_regions`/`exclusion_regions`, `method`, and so on) - everything
297
+ available via `main()`/the CLI is available here too, just without the file layer.
298
+
299
+ ## Scoring sequences your own way, instead of CAI/tAI
300
+
301
+ By default, optimization scores codon choices against a codon-usage table (CAI/tAI-style,
302
+ via `organism_name`). To score sequences with your own logic instead, write a Python file
303
+ defining a `score(seq)` function (`seq` is a plain DNA string like `"ATGCGT..."`, the whole
304
+ ORF being optimized; return a number, higher = better) - copy
305
+ [`examples/custom_score_template.py`](examples/custom_score_template.py) as a starting
306
+ point.
307
+
308
+ From the command line:
309
+
310
+ ```bash
311
+ eso-optimize --input-folder path/to/fasta_files --custom-score-file my_score.py
312
+ ```
313
+
314
+ (`--custom-score-file` overrides `--organism-name`/`--method`.) A `--custom-score-file`
315
+ with a mistake in it - a missing `score` function, a typo, a function that crashes or
316
+ returns the wrong type - fails immediately with a plain-English message, before any
317
+ optimization runs, rather than surfacing later as a Python traceback.
318
+
319
+ From Python, either call `optimization_engine`/`eso.pipeline.main` directly with
320
+ `custom_score_fn`/`custom_score_minimize`, or reuse the same file-loading + validation the
321
+ CLI uses via `eso.custom_score.load_custom_score_from_file`:
322
+
323
+ ```python
324
+ from eso.custom_score import load_custom_score_from_file
325
+ from eso.optimize import optimization_engine
326
+
327
+ score_fn = load_custom_score_from_file("my_score.py") # same validation as the CLI
328
+ final_seq, _, _ = optimization_engine(seq, custom_score_fn=score_fn)
329
+
330
+ # or, skipping the file entirely:
331
+ final_seq, _, _ = optimization_engine(
332
+ seq,
333
+ custom_score_fn=lambda whole_orf: whole_orf.count("G") + whole_orf.count("C"),
334
+ )
335
+ ```
336
+
337
+ `custom_score_fn` is called once on the whole ORF being optimized, on every trial mutation
338
+ tried during `optimize()` - this can be slow for a long sequence or an expensive function
339
+ (a warning is raised). `custom_score_minimize=True` (`--custom-score-minimize` on the CLI)
340
+ treats a *lower* `custom_score_fn` value as better, instead of higher.
341
+
342
+ **Scope**: custom scoring is automatically restricted to `orf_regions` (one scored region
343
+ per ORF, matching how the built-in CAI/tAI codon-usage scoring is already scoped via
344
+ DNAChisel's `CodonOptimize(location=orf, ...)`) - `custom_score_fn` never sees any non-ORF
345
+ flanking sequence (UTRs, locked/excluded regions). If you don't pass `orf_regions`, this
346
+ is the whole sequence (trimmed to a multiple of 3), same as everywhere else in ESO.
347
+
348
+ **An earlier version of this feature also supported a "windowed" mode** (scoring
349
+ fixed-size chunks and summing them, mirroring how the built-in CAI/tAI scoring works
350
+ internally) as a claimed speed optimization. It was removed after benchmarking found no
351
+ case where it was actually faster than the whole-ORF evaluation above - comparable at
352
+ best, meaningfully slower at worst, since DNAChisel's own optimizer ends up calling the
353
+ score function considerably more often when it's chunk-localizable - while carrying a
354
+ real, unpreventable correctness risk (a score that doesn't genuinely decompose per-chunk,
355
+ true of most real external/ML models, would silently compute a different, structurally
356
+ unrelated quantity, with no reliable way to detect this automatically). See
357
+ `docs/detector-comparisons.md` for the full investigation, including an initial benchmark
358
+ that was itself flawed and had to be corrected before the removal decision was made.
359
+
360
+ ## Restricting ORF and exclusion regions per sequence
361
+
362
+ By default, the entire sequence is treated as one in-frame, translation-preserving ORF
363
+ with nothing locked. To instead give each sequence its own ORF region(s) (e.g. skip a
364
+ UTR) and/or exclusion regions that must never be edited (e.g. a known regulatory
365
+ element), pass `indexes` (from Python) or `--indexes-file` (from the CLI).
366
+
367
+ From the command line, point at a JSON file - copy
368
+ [`examples/indexes_template.json`](examples/indexes_template.json) as a starting point:
369
+
370
+ ```json
371
+ [
372
+ {
373
+ "file": "my_gene",
374
+ "seq_index": "0",
375
+ "orf_regions": "1-6, 51-68",
376
+ "exclusion_regions": "1-6, 50-68"
377
+ }
378
+ ]
379
+ ```
380
+
381
+ ```bash
382
+ eso-optimize --input-folder path/to/fasta_files --indexes-file indexes.json
383
+ ```
384
+
385
+ - `file` is the FASTA/GenBank file's stem (no extension, e.g. `"my_gene"` for
386
+ `my_gene.fasta`).
387
+ - `seq_index` is which record within that file, 0-indexed in file order, as a string.
388
+ - `orf_regions`/`exclusion_regions` are 1-indexed, inclusive region strings (e.g.
389
+ `"1-6, 51-68"` for two separate regions); omit `exclusion_regions` (or use `""`) for no
390
+ exclusions.
391
+
392
+ A malformed `--indexes-file` (bad JSON, missing `file`/`seq_index`) fails immediately with
393
+ a plain-English message; malformed region strings themselves are validated the same way
394
+ whether `indexes` came from a file or was passed directly to `eso.pipeline.main`/`main`.
395
+
396
+ From Python, pass the equivalent dict directly:
397
+
398
+ ```python
399
+ from eso import main
400
+
401
+ message, results = main(
402
+ input_folder="path/to/fasta_files",
403
+ indexes={("my_gene", "0"): ("1-6, 51-68", "1-6, 50-68")},
404
+ )
405
+ ```
406
+
407
+ ## Motif sources: methylation, cryptic ribosome binding, cryptic promoters, and your own
408
+
409
+ Motif detection (`--compute-motifs`) isn't only about methylation - the same PSSM-based
410
+ scanner (`eso.detection.methylation.find_motif_sites`) works for any short sequence motif
411
+ you want to flag. Needs at least one motif, from any combination of three sources:
412
+
413
+ - **A MEME-minimal-format PSSM file** (`--motifs-path` / `motifs_path=`) - the original
414
+ option, for a curated or experimentally-derived motif set you already have as a file.
415
+ - **Bundled common motifs** (`--common-motifs dam,dcm` / `common_motifs=["dam", "dcm"]`)
416
+ - no file needed. See [`eso/detection/common_motifs.py`](eso/detection/common_motifs.py)
417
+ (`COMMON_MOTIFS`) for the full list and sources; currently:
418
+ - **Methylation** (E. coli, already a first-class host here - see `eso.codon_usage`'s
419
+ bundled `e_coli` table): `dam` (GATC, N6-methyladenine), `dcm` (CCWGG,
420
+ C5-methylcytosine on the internal C).
421
+ - **Cryptic ribosome binding**: `shine_dalgarno` (AGGAGG) - flags a copy of the
422
+ bacterial RBS consensus occurring *inside* a coding region, a known source of
423
+ unintended internal translation initiation.
424
+ - **Cryptic bacterial promoter elements**: `sigma70_minus35` (TTGACA),
425
+ `sigma70_minus10` (TATAAT) - the two sigma70 hexamers; an accidental occurrence of
426
+ either inside a coding sequence is a classic source of unwanted transcription. Each
427
+ hexamer is flagged independently (a real promoter needs both, correctly spaced,
428
+ which this detector doesn't check for) - treat an isolated hit as a coarse screen,
429
+ not a confirmed cryptic promoter.
430
+ - **Not included, and why** (see the module docstring for the full explanation):
431
+ transcription terminators (a secondary-structure property, not a fixed linear
432
+ motif), the Kozak sequence (something to match, not avoid - a different problem),
433
+ and restriction enzyme sites (already covered comprehensively by DNAChisel's own
434
+ `dnachisel.list_common_enzymes()` / `EnzymeSitePattern`, usable directly with
435
+ `AvoidPattern` during optimization - no need to duplicate it here).
436
+ - **Your own IUPAC consensus string** - the easy custom-motif path, no PSSM/MEME file
437
+ needed:
438
+
439
+ ```python
440
+ from eso.detection.motif_utils import motif_from_consensus, motifs_from_consensus_dict
441
+
442
+ my_motif = motif_from_consensus("my_site", "GANTC") # N = any base, standard IUPAC codes
443
+ # or define several at once:
444
+ my_motifs = motifs_from_consensus_dict({"site_a": "GATC", "site_b": "CCWGG"})
445
+ ```
446
+
447
+ Pass the result as `relevant_motifs` to `eso.detection.methylation.find_motif_sites`
448
+ directly, or combine with the other two sources yourself before calling it - there's no
449
+ CLI flag for this one yet (a MEME file is still the CLI's path for anything beyond the
450
+ bundled common motifs).
451
+
452
+ Most published methylation/restriction motifs (REBASE, NEB's technical notes, the primary
453
+ literature) are naturally given exactly this way - a short consensus sequence with IUPAC
454
+ ambiguity codes (`W` = A or T, `N` = any base, etc.) - not as a position-probability
455
+ matrix, so this avoids hand-authoring a MEME file just to check for one. For organisms
456
+ beyond E. coli, [REBASE](http://rebase.neb.com) is the standard reference database for
457
+ methylation motifs - look up the motif there and pass it straight to
458
+ `motif_from_consensus`.
459
+
460
+ ## Development
461
+
462
+ ```bash
463
+ poetry install --with dev
464
+ pytest
465
+ ```
466
+
467
+ See [`CONTRIBUTING.md`](CONTRIBUTING.md) for this repo's testing/documentation
468
+ conventions, how the two independently-developed detector implementations
469
+ relate to each other, and how to report a bug.
470
+
471
+ ## Citation
472
+
473
+ If you use this tool, please cite the paper it implements:
474
+
475
+ > Menuhin-Gruman, I., Arbel, M., Amitay, N., Sionov, K., Naki, D., Katzir, I., Edgar, O.,
476
+ > Bergman, S., & Tuller, T. (2022). Evolutionary Stability Optimizer (ESO): A Novel
477
+ > Approach to Identify and Avoid Mutational Hotspots in DNA Sequences While Maintaining
478
+ > High Expression Levels. *ACS Synthetic Biology*, 11(3), 1142-1151.
479
+ > https://doi.org/10.1021/acssynbio.1c00426
480
+
481
+ ```bibtex
482
+ @article{menuhingruman2022eso,
483
+ title = {Evolutionary Stability Optimizer (ESO): A Novel Approach to Identify and
484
+ Avoid Mutational Hotspots in DNA Sequences While Maintaining High
485
+ Expression Levels},
486
+ author = {Menuhin-Gruman, Itamar and Arbel, Matan and Amitay, Niv and Sionov, Karin
487
+ and Naki, Doron and Katzir, Itai and Edgar, Omer and Bergman, Shaked
488
+ and Tuller, Tamir},
489
+ journal = {ACS Synthetic Biology},
490
+ volume = {11},
491
+ number = {3},
492
+ pages = {1142--1151},
493
+ year = {2022},
494
+ doi = {10.1021/acssynbio.1c00426}
495
+ }
496
+ ```
497
+
498
+ ## License
499
+
500
+ [MIT](LICENSE)
501
+