Metadata-Version: 2.5
Name: ledidi
Version: 2.2.0
Summary: Ledidi is an optimization approach for designing edits to biological sequences.
Project-URL: Homepage, https://github.com/jmschrei/ledidi
Project-URL: Documentation, https://ledidi.readthedocs.io/en/latest/
Project-URL: Repository, https://github.com/jmschrei/ledidi
Author: Yang Lu
Author-email: Jacob Schreiber <jmschreiber91@gmail.com>
License:                                  Apache License
                                   Version 2.0, January 2004
                                http://www.apache.org/licenses/
        
           TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
        
           1. Definitions.
        
              "License" shall mean the terms and conditions for use, reproduction,
              and distribution as defined by Sections 1 through 9 of this document.
        
              "Licensor" shall mean the copyright owner or entity authorized by
              the copyright owner that is granting the License.
        
              "Legal Entity" shall mean the union of the acting entity and all
              other entities that control, are controlled by, or are under common
              control with that entity. For the purposes of this definition,
              "control" means (i) the power, direct or indirect, to cause the
              direction or management of such entity, whether by contract or
              otherwise, or (ii) ownership of fifty percent (50%) or more of the
              outstanding shares, or (iii) beneficial ownership of such entity.
        
              "You" (or "Your") shall mean an individual or Legal Entity
              exercising permissions granted by this License.
        
              "Source" form shall mean the preferred form for making modifications,
              including but not limited to software source code, documentation
              source, and configuration files.
        
              "Object" form shall mean any form resulting from mechanical
              transformation or translation of a Source form, including but
              not limited to compiled object code, generated documentation,
              and conversions to other media types.
        
              "Work" shall mean the work of authorship, whether in Source or
              Object form, made available under the License, as indicated by a
              copyright notice that is included in or attached to the work
              (an example is provided in the Appendix below).
        
              "Derivative Works" shall mean any work, whether in Source or Object
              form, that is based on (or derived from) the Work and for which the
              editorial revisions, annotations, elaborations, or other modifications
              represent, as a whole, an original work of authorship. For the purposes
              of this License, Derivative Works shall not include works that remain
              separable from, or merely link (or bind by name) to the interfaces of,
              the Work and Derivative Works thereof.
        
              "Contribution" shall mean any work of authorship, including
              the original version of the Work and any modifications or additions
              to that Work or Derivative Works thereof, that is intentionally
              submitted to Licensor for inclusion in the Work by the copyright owner
              or by an individual or Legal Entity authorized to submit on behalf of
              the copyright owner. For the purposes of this definition, "submitted"
              means any form of electronic, verbal, or written communication sent
              to the Licensor or its representatives, including but not limited to
              communication on electronic mailing lists, source code control systems,
              and issue tracking systems that are managed by, or on behalf of, the
              Licensor for the purpose of discussing and improving the Work, but
              excluding communication that is conspicuously marked or otherwise
              designated in writing by the copyright owner as "Not a Contribution."
        
              "Contributor" shall mean Licensor and any individual or Legal Entity
              on behalf of whom a Contribution has been received by Licensor and
              subsequently incorporated within the Work.
        
           2. Grant of Copyright License. Subject to the terms and conditions of
              this License, each Contributor hereby grants to You a perpetual,
              worldwide, non-exclusive, no-charge, royalty-free, irrevocable
              copyright license to reproduce, prepare Derivative Works of,
              publicly display, publicly perform, sublicense, and distribute the
              Work and such Derivative Works in Source or Object form.
        
           3. Grant of Patent License. Subject to the terms and conditions of
              this License, each Contributor hereby grants to You a perpetual,
              worldwide, non-exclusive, no-charge, royalty-free, irrevocable
              (except as stated in this section) patent license to make, have made,
              use, offer to sell, sell, import, and otherwise transfer the Work,
              where such license applies only to those patent claims licensable
              by such Contributor that are necessarily infringed by their
              Contribution(s) alone or by combination of their Contribution(s)
              with the Work to which such Contribution(s) was submitted. If You
              institute patent litigation against any entity (including a
              cross-claim or counterclaim in a lawsuit) alleging that the Work
              or a Contribution incorporated within the Work constitutes direct
              or contributory patent infringement, then any patent licenses
              granted to You under this License for that Work shall terminate
              as of the date such litigation is filed.
        
           4. Redistribution. You may reproduce and distribute copies of the
              Work or Derivative Works thereof in any medium, with or without
              modifications, and in Source or Object form, provided that You
              meet the following conditions:
        
              (a) You must give any other recipients of the Work or
                  Derivative Works a copy of this License; and
        
              (b) You must cause any modified files to carry prominent notices
                  stating that You changed the files; and
        
              (c) You must retain, in the Source form of any Derivative Works
                  that You distribute, all copyright, patent, trademark, and
                  attribution notices from the Source form of the Work,
                  excluding those notices that do not pertain to any part of
                  the Derivative Works; and
        
              (d) If the Work includes a "NOTICE" text file as part of its
                  distribution, then any Derivative Works that You distribute must
                  include a readable copy of the attribution notices contained
                  within such NOTICE file, excluding those notices that do not
                  pertain to any part of the Derivative Works, in at least one
                  of the following places: within a NOTICE text file distributed
                  as part of the Derivative Works; within the Source form or
                  documentation, if provided along with the Derivative Works; or,
                  within a display generated by the Derivative Works, if and
                  wherever such third-party notices normally appear. The contents
                  of the NOTICE file are for informational purposes only and
                  do not modify the License. You may add Your own attribution
                  notices within Derivative Works that You distribute, alongside
                  or as an addendum to the NOTICE text from the Work, provided
                  that such additional attribution notices cannot be construed
                  as modifying the License.
        
              You may add Your own copyright statement to Your modifications and
              may provide additional or different license terms and conditions
              for use, reproduction, or distribution of Your modifications, or
              for any such Derivative Works as a whole, provided Your use,
              reproduction, and distribution of the Work otherwise complies with
              the conditions stated in this License.
        
           5. Submission of Contributions. Unless You explicitly state otherwise,
              any Contribution intentionally submitted for inclusion in the Work
              by You to the Licensor shall be under the terms and conditions of
              this License, without any additional terms or conditions.
              Notwithstanding the above, nothing herein shall supersede or modify
              the terms of any separate license agreement you may have executed
              with Licensor regarding such Contributions.
        
           6. Trademarks. This License does not grant permission to use the trade
              names, trademarks, service marks, or product names of the Licensor,
              except as required for reasonable and customary use in describing the
              origin of the Work and reproducing the content of the NOTICE file.
        
           7. Disclaimer of Warranty. Unless required by applicable law or
              agreed to in writing, Licensor provides the Work (and each
              Contributor provides its Contributions) on an "AS IS" BASIS,
              WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
              implied, including, without limitation, any warranties or conditions
              of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
              PARTICULAR PURPOSE. You are solely responsible for determining the
              appropriateness of using or redistributing the Work and assume any
              risks associated with Your exercise of permissions under this License.
        
           8. Limitation of Liability. In no event and under no legal theory,
              whether in tort (including negligence), contract, or otherwise,
              unless required by applicable law (such as deliberate and grossly
              negligent acts) or agreed to in writing, shall any Contributor be
              liable to You for damages, including any direct, indirect, special,
              incidental, or consequential damages of any character arising as a
              result of this License or out of the use or inability to use the
              Work (including but not limited to damages for loss of goodwill,
              work stoppage, computer failure or malfunction, or any and all
              other commercial damages or losses), even if such Contributor
              has been advised of the possibility of such damages.
        
           9. Accepting Warranty or Additional Liability. While redistributing
              the Work or Derivative Works thereof, You may choose to offer,
              and charge a fee for, acceptance of support, warranty, indemnity,
              or other liability obligations and/or rights consistent with this
              License. However, in accepting such obligations, You may act only
              on Your own behalf and on Your sole responsibility, not on behalf
              of any other Contributor, and only if You agree to indemnify,
              defend, and hold each Contributor harmless for any liability
              incurred by, or claims asserted against, such Contributor by reason
              of your accepting any such warranty or additional liability.
        
           END OF TERMS AND CONDITIONS
        
           APPENDIX: How to apply the Apache License to your work.
        
              To apply the Apache License to your work, attach the following
              boilerplate notice, with the fields enclosed by brackets "[]"
              replaced with your own identifying information. (Don't include
              the brackets!)  The text should be enclosed in the appropriate
              comment syntax for the file format. We also recommend that a
              file or class name and description of purpose be included on the
              same "printed page" as the copyright notice for easier
              identification within third-party archives.
        
           Copyright [yyyy] [name of copyright owner]
        
           Licensed under the Apache License, Version 2.0 (the "License");
           you may not use this file except in compliance with the License.
           You may obtain a copy of the License at
        
               http://www.apache.org/licenses/LICENSE-2.0
        
           Unless required by applicable law or agreed to in writing, software
           distributed under the License is distributed on an "AS IS" BASIS,
           WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
           See the License for the specific language governing permissions and
           limitations under the License.
License-File: LICENSE
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: >=3.10
Requires-Dist: matplotlib
Requires-Dist: numpy
Requires-Dist: tangermeme>=1.3.0
Requires-Dist: torch>=2.0
Provides-Extra: dev
Requires-Dist: pytest; extra == 'dev'
Requires-Dist: pytest-cov; extra == 'dev'
Description-Content-Type: text/markdown

# Ledidi

[![PyPI version](https://img.shields.io/pypi/v/ledidi.svg)](https://pypi.org/project/ledidi/)
[![Python versions](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://pypi.org/project/ledidi/)
[![Tests](https://github.com/jmschrei/ledidi/actions/workflows/test.yml/badge.svg)](https://github.com/jmschrei/ledidi/actions/workflows/test.yml)
[![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen.svg)](https://github.com/jmschrei/ledidi/actions/workflows/test.yml)
[![Documentation Status](https://readthedocs.org/projects/ledidi/badge/?version=latest)](https://ledidi.readthedocs.io/en/latest/)
[![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://github.com/jmschrei/ledidi/blob/master/LICENSE)
[![PyPI Downloads](https://img.shields.io/pypi/dm/ledidi.svg)](https://pepy.tech/projects/Ledidi)

<img src="https://github.com/user-attachments/assets/500e5f18-f9af-4cc9-b76c-23296d60640d" width="40%" height="40%">

[[preprint](https://www.biorxiv.org/content/10.1101/2025.04.22.650035v1)][[docs](https://ledidi.readthedocs.io/en/latest/)]

Ledidi is an approach for designing edits to biological sequences that induce desired properties. It does this by inverting the normal way that one thinks about machine learning models: normally during training, the data is held constant and model weights are updated; here, Ledidi holds the model constant and updates the *data*. When using Ledidi, the data are the sequences that are being edited and the "updates" to these sequences are the edits. Using this simple paradigm, Ledidi *turns any trained machine learning model into a biological sequence editor*, regardless of the original purpose of the model. 

A challenge with designing edits for categorical sequences (e.g., DNA sequences with four possible nucleotides) is that the inputs are usually one-hot encoded but gradients are continuous. Applying the gradients directly would yield oddities like 1.2 of an A and -0.3 of a G. These values yield two problems: (1) such values are not biologically meaningful as you cannot design a sequence that is "1.2 A and -0.3 G" and (2) because most machine learning models have been trained on one-hot encoded data and are calibrated for that distribution, such sequences would likely confuse the model, push it off the training data manifold, and result in anomalous sequences even if one managed to turn the output back into a valid sequence.

Ledidi resolves this challenge by phrasing edit design as an optimization problem over a continuous weight matrix `W` that gradients *can* be directly applied to. Specifically, at each iteration, Ledidi draws one-hot encoded sequences one position at a time from a Gumbel-softmax distribution defined by `log(X + eps) + W` where `X` is the original one-hot encoded sequence and `eps` is a small value. When the drawn character is different from the character in `X`, likely because `W` has been updated at that position to prefer another character, an "edit" is being made. These edited sequences are then passed through the frozen pre-trained model and the output is compared to the desired output. The total loss is then derived from this output loss as well as an input loss that is the count of proposed edits, limiting the number that Ledidi will propose. A gradient is calculated from this loss and `W` is updated, keeping `X` unchanged.

![image](https://github.com/user-attachments/assets/41ffe180-8171-4f28-a88e-41cc1c79985a)

Applied out-of-the-box across dozens of sequence-based ML models, Ledidi can take a sequence that does not exhibit some form of activity (e.g., TF binding, transcription, etc) and design edits that turn it into one exhibiting strong activity! Despite major differences in these models, this design does not require any hyperparameter tuning to get good results, and only requires balancing the input and output loss in a simple way to get the best results.

<img width="1367" alt="image" src="https://github.com/user-attachments/assets/39edd8fc-be58-4fb3-8006-18401fc81336" />

Although our examples right now are largely nucleotide sequence-based, one can also apply Ledidi out-of-the-box to RNA or protein models (or really, to any model with a sequence of categorical inputs such as small molecules). The only limitation is what will fit in your GPU or what you are willing to wait for on your CPU!

### Installation
`pip install ledidi`

If you already have PyTorch installed, this should take less than a minute. Otherwise, it may take several minutes, of which most of the time is spent installing PyTorch.

To install from source for development, Ledidi is packaged with a `pyproject.toml` and is built with [uv](https://docs.astral.sh/uv/):

```
git clone https://github.com/jmschrei/ledidi
cd ledidi
uv pip install -e ".[dev]"
```

The test suite runs on CPU with `python -m pytest tests/`.

On a machine without a GPU, install the much smaller CPU-only PyTorch build first and then Ledidi:

```
pip install --index-url https://download.pytorch.org/whl/cpu torch
pip install ledidi
```

Note that `ledidi` defaults to `device='cuda'`, so on such a machine you must pass `device='cpu'` explicitly. See the [installation guide](https://ledidi.readthedocs.io/en/latest/installation.html) for the full walkthrough — virtual environments, conda, installing from source, confirming the install works, and troubleshooting.

### Requirements

**Software.** Python >= 3.10 and PyTorch >= 2.0. Installing Ledidi pulls in [tangermeme](https://github.com/jmschrei/tangermeme) (>= 1.3.0), `numpy`, and `matplotlib` automatically, and `tangermeme` brings a scientific stack of its own (36 packages in a clean environment); nothing else needs installing to design sequences. The tutorials additionally use whichever oracle package they demonstrate (`bpnetlite`, `enformer-pytorch`, `boda`), but Tutorials 0 and 8 run with no extra packages and no downloads.

**Hardware.** Ledidi runs on a CPU or a CUDA GPU, and there is no minimum GPU — the toy example below finishes in about three seconds on a single CPU thread. Note that `ledidi` defaults to `device='cuda'`, so on a machine without a GPU you must pass `device='cpu'` explicitly. Memory is dominated by the oracle rather than by Ledidi: a BPNet-scale design over a 2114 bp sequence peaks at 232 MB of GPU memory at the default `batch_size=16`, scaling roughly linearly in `batch_size`. The same design takes 0.8s on a GPU against roughly 30s on a single CPU thread (8s if PyTorch is given eight). Large long-context oracles are the case that actually strains a card.

See the [requirements page](https://ledidi.readthedocs.io/en/latest/requirements.html) for the full hardware and software requirements, measured memory and runtime tables, and a snippet that verifies your installation end to end.

### Using Ledidi with a coding agent

Ledidi ships a [Claude Code Agent Skill](https://docs.claude.com/en/docs/claude-code/skills) that teaches an agent the design objective, the oracle-model contract, and the footguns that silently produce bad designs. Install it once and it applies in every project:

```
ledidi-install-skills
```

This copies the skill into `~/.claude/skills/ledidi` (use `--force` to upgrade after updating Ledidi, or `--print-path` to see the bundled source). It is a router: a short `SKILL.md` plus reference files the agent reads on demand for the topic at hand — the objective and the `l` trade-off, multi-task and multi-model oracles, models whose input window differs from the design field, constraints and priors, in-painting, custom losses, affinity catalogs, pruning, validating designs, and out-of-memory recovery.

Ledidi depends on [tangermeme](https://github.com/jmschrei/tangermeme), which ships its own skill covering attribution, motif scanning, and sequence I/O. Installing both is recommended, since validating a design leans on them:

```
tangermeme-install-skills
```

### Quickstart

Here is a complete, runnable example that uses a tiny parameter-free oracle, so there is nothing to download. The oracle scores how well a sequence matches the AP-1 motif `TGACTCA`, and Ledidi designs the edits that maximize that score.

```python
import torch
from ledidi import ledidi

# A toy oracle: it slides the AP-1 motif TGACTCA across the sequence and returns
# the best match (a perfect match scores 7). Any differentiable torch model that
# takes a one-hot sequence and returns a prediction can be used in its place.
motif = "TGACTCA"
weights = torch.zeros(1, 4, len(motif))
for i, char in enumerate(motif):
	weights[0, "ACGT".index(char), i] = 1.0

class MotifScore(torch.nn.Module):
	def forward(self, X):
		return torch.nn.functional.conv1d(X, weights).amax(dim=-1)

# A random one-hot starting sequence of shape (1, 4, length).
torch.manual_seed(0)
idxs = torch.randint(0, 4, (1, 50))
X = torch.zeros(1, 4, 50).scatter_(1, idxs.unsqueeze(1), 1.0)

# Ask Ledidi for edits that make the oracle output a perfect match (7).
y_bar = torch.tensor([[7.0]])
X_hat = ledidi(MotifScore(), X, y_bar, device="cpu", random_state=0, verbose=False)

# X_hat has shape (batch_size, 4, length); decode the first designed sequence.
designed = "".join("ACGT"[c] for c in X_hat[0].argmax(dim=0))
print("TGACTCA" in designed)  # True -- Ledidi inserted the motif in just a couple of edits
```

Ledidi finds the cheapest place to introduce the motif and edits only the positions it needs to, rather than overwriting a whole stretch of sequence.

### Input and output format

Ledidi works with one-hot encoded sequences as `torch.float32` tensors of shape `(1, n_channels, length)` — for DNA that is `(1, 4, length)`, with channels ordered `A, C, G, T`. The returned `X_hat` has shape `(batch_size, n_channels, length)`: a batch of independently sampled designed sequences (`batch_size` defaults to 16).

If you are starting from a string, you can one-hot encode it and decode the result directly:

```python
import torch

def one_hot_encode(seq):
	mapping = {"A": 0, "C": 1, "G": 2, "T": 3}
	X = torch.zeros(1, 4, len(seq))
	for i, char in enumerate(seq):
		X[0, mapping[char], i] = 1.0
	return X

def decode(X):  # X is a single (4, length) one-hot sequence
	return "".join("ACGT"[c] for c in X.argmax(dim=0))

X = one_hot_encode("ACGTACGTACGT")
# ... run Ledidi to get X_hat ...
# decode(X_hat[0]) turns the first designed sequence back into a string
```

The sibling library [tangermeme](https://github.com/jmschrei/tangermeme) provides `one_hot_encode` and `characters` utilities that handle this (and reading FASTA/loci) for you.

### Running on CPU or GPU

By default `ledidi(...)` moves the model and tensors to the GPU (`device='cuda'`). On a machine without a CUDA GPU you must pass `device='cpu'` explicitly, otherwise the call will error:

```python
X_hat = ledidi(model, X, y_bar, device='cpu')
```

### Usage

Please see the [documentation site](https://ledidi.readthedocs.io/en/latest/) for more complete tutorials on how to use Ledidi. You can find some example BPNet models -- including the one used in the tutorials -- at https://zenodo.org/records/14604495.

At a high level, Ledidi requires a sequence-based machine learning model, an initial sequence, and a desired output from the model. If the model is multi-task, this desired output does not need to cover all of the tasks and can even just use a single one (see this tutorial on [PyTorch wrappers](https://tangermeme.readthedocs.io/en/latest/vignettes/Wrappers_are_Productivity_Hacks.html) for practical advice on how to manipulate models to make them easier to work with Ledidi). These three items are then passed into `ledidi` for design! Note that there is also a `Ledidi` object which is an internal wrapper that you typically won't need to use directly.

The example below uses a real BPNet oracle that predicts GATA2 binding. It requires `bpnetlite` (`pip install bpnetlite`) and downloads a small (~0.5 MB) model from Zenodo on first run, and the design takes roughly half a minute on a single CPU thread, less if PyTorch is given more. For a dependency-free example that runs anywhere with no downloads, see the [Quickstart](#quickstart).

```python
import torch
import urllib.request
from bpnetlite.bpnet import ControlWrapper
from bpnetlite.bpnet import CountWrapper
from ledidi import ledidi

# Download a small (~0.5 MB) BPNet model that predicts GATA2 binding.
url = "https://zenodo.org/api/records/14604495/files/GATA2.torch/content"
urllib.request.urlretrieve(url, "GATA2.torch")

model = torch.load("GATA2.torch", weights_only=False, map_location="cpu")
model = CountWrapper(ControlWrapper(model))  # use only the predicted count

# A random 2114 bp one-hot sequence (BPNet's input length) that the model
# predicts essentially no binding for -- the region we want to design into.
torch.manual_seed(0)
idxs = torch.randint(0, 4, (1, 2114))
X = torch.zeros(1, 4, 2114).scatter_(1, idxs.unsqueeze(1), 1.0)

y_bar = torch.tensor([[4.5]])  # ask for a high predicted count
X_hat = ledidi(model, X, y_bar, device="cpu", random_state=0)  # ~30s on 1 CPU thread
```

The `ledidi` method will return a batch of sequences whose size can be controlled with the `batch_size` parameter. These sequences are independently generated by sampling from the underlying Gumbel-softmax distributions learned by Ledidi. Importantly, each batch of edits will be correlated in the sense that they all come from the single set of Ledidi weights. So, if a motif could have been substituted in at multiple locations Ledidi will end up only choosing one and these edits will all be drawn from that choice. <b>If you want many different sets of edits, run Ledidi multiple times — either with `n_repeats` to do it in a single call, or by re-running with a different `random_state` each time.</b> However, an upside is that once the Ledidi weight matrix is learned, edited sequences can be sampled extremely quickly using the forward function.

```python
X_hat = ledidi(model, X, y_bar, n_samples=10000)  # Runs almost as fast as the default!
```

If we take a look at the edits being proposed in this sequence, we can see that frequently Ledidi can make designs with fewer edits than the length of the motif that should be inserted. Essentially, Ledidi automatically finds locations on the sequence that are close to the motifs that need to be inserted and precisely makes edits there rather than forcing an entire motif in somewhere in the sequence. Here we can see attributions from our GATA prediction model before edits are made (low because GATA does not bind to this example region, see the tutorial), after edits are made with the edited positions shown in orange, and after a [greedy pruning algorithm](#multiple-models-custom-losses-and-pruning) is used to trim the edit space down in magenta.

<img src="https://github.com/user-attachments/assets/6ea64ed7-bfad-479b-a433-116ad7465ba6" width="70%" height="70%">

When `verbose` is set to `True` you will get logs showing the input and output losses, and the total loss from combining them with the mixing weight. The logs may look something like this:

![image](https://github.com/user-attachments/assets/35040f80-a997-46f2-9855-a4db1a8337b6)

This means that by iteration 100, an average of 35.75 edits were proposed per sequence, and that the best iteration at convergence only had 28.19 edits per sequence on average. The output loss went from 16 to 0.18, which is a pretty major drop in the loss. If you pass in `return_history=True` you can get these losses over the course of the entire process. Here is an example from the tutorial.

<img src="https://github.com/user-attachments/assets/e9801dbc-1821-476a-9b1b-1962c0552f29" width="60%" height="60%">

You can also look at where edits were proposed across the entire process. This gives you a sense for how long edits persisted across the process. In this example, tons of edits are proposed initially and then many of them are undone because the input loss tries to minimize this number.

<img src="https://github.com/user-attachments/assets/f115605e-cc26-4ba8-b3ed-ff0b336f06b4" width="60%" height="60%">

### Key parameters

Most designs only require tuning a couple of knobs. These are passed straight through `ledidi(...)`:

| Parameter | Default | What it does |
|---|---|---|
| `l` | `0.1` | Weight on the edit (input) loss. **The main knob to tune** — lower values prioritize hitting the target output, higher values prioritize making fewer edits. |
| `target` | `None` | For a multi-task model, the index of the output to design against. `None` uses the whole output. |
| `output_loss` | `MSELoss()` | The loss comparing the model's prediction to `y_bar`. Swap in any callable `f(y_hat, y_bar)`. |
| `tau` | `1` | Gumbel-softmax temperature. Affects only the gradient, not the sampled sequence (which is one-hot at every `tau`). Rarely worth tuning. |
| `batch_size` | `16` | Sequences sampled and averaged per iteration. |
| `max_iter` | `1000` | Maximum optimization iterations. |
| `early_stopping_iter` | `100` | Stop after this many iterations without improvement. |
| `input_mask` | `None` | Boolean mask, length `length`, of positions that may **not** be edited. |
| `random_state` | `None` | Seed for reproducible sampling. Unlike calling `torch.manual_seed`, this does not mutate the global RNG, so it leaves the rest of your script unaffected. |
| `device` | `'cuda'` | Device to run on (pass `'cpu'` if you have no GPU). |
| `verbose` | `True` | Print the input, output, and total loss as the design proceeds. |

A few parameters live on `ledidi()` itself rather than being passed through: `n_samples` (draw this many designed sequences after optimization), `n_repeats` (run the whole procedure multiple times), and `return_history` / `return_designer` (also return the loss history / the fitted designer).

### Multiple models, custom losses, and pruning

The top-level package exports `ledidi` and `Ledidi`; the other tools live in their own submodules.

**Balance several oracles** by combining them with `DesignWrapper`, which concatenates their predictions. Pass a vector `y_bar` with one target per model — for example, to raise one model's output while holding another's fixed:

```python
from ledidi.wrappers import DesignWrapper

model = DesignWrapper([model_a, model_b])  # predictions concatenated along the last axis
X_hat = ledidi(model, X, y_bar, device="cpu")  # y_bar has one entry per model output
```

**Design cell-type-specific elements** with the `MinGap` loss, which maximizes a set of on-target outputs while minimizing the off-target ones. It ignores `y_bar` (still required by the signature, so pass a placeholder):

```python
import torch
from ledidi.losses import MinGap

in_mask = torch.tensor([True, False, False])  # output 0 on-target; 1 and 2 off-target
X_hat = ledidi(model, X, torch.zeros(1, 3), output_loss=MinGap(in_mask), device="cpu")
```

**Trim unnecessary edits** after design with `greedy_pruning`, which removes edits one at a time as long as each removal changes the model output by less than `threshold`:

```python
from ledidi.pruning import greedy_pruning

X_pruned = greedy_pruning(model, X, X_hat[:1], threshold=1)  # prunes a single sequence
```

There are also plotting helpers in `ledidi.plot` (`plot_edits`, `plot_history`).

### Inspecting the edits

To see exactly which positions changed and how, compare the original sequence to a designed one:

```python
seq = X_hat[0]  # one designed sequence, shape (4, length)
positions = torch.where((X[0] != seq).any(dim=0))[0]
for p in positions:
	before = "ACGT"[X[0, :, p].argmax()]
	after  = "ACGT"[seq[:, p].argmax()]
	print(f"position {p.item()}: {before} -> {after}")
```

For the Quickstart example this prints just two edits, the ones that complete the `TGACTCA` motif. Each of the `batch_size` sequences in `X_hat` is sampled independently, so they may carry slightly different edits.

### Tutorials

Worked examples as Jupyter notebooks (also rendered on the [documentation site](https://ledidi.readthedocs.io/en/latest/)). Tutorial 0 and 8 run anywhere on a CPU with no downloads; the rest use BPNet oracle models available at https://zenodo.org/records/14604495.

0. [A First Design](https://github.com/jmschrei/ledidi/blob/master/docs/tutorials/Tutorial_0_-_Getting_Started.ipynb)
1. [Design of Protein Binding Sites](https://github.com/jmschrei/ledidi/blob/master/docs/tutorials/Tutorial_1_-_Design_of_Protein_Binding_Sites.ipynb)
2. [Constraints and Priors](https://github.com/jmschrei/ledidi/blob/master/docs/tutorials/Tutorial_2_-_Constraints_and_Priors.ipynb)
3. [In-Painting](https://github.com/jmschrei/ledidi/blob/master/docs/tutorials/Tutorial_3_-_In-Painting.ipynb)
4. [Multiple Models](https://github.com/jmschrei/ledidi/blob/master/docs/tutorials/Tutorial_4_-_Multiple_Models.ipynb)
5. [Affinity Catalogs](https://github.com/jmschrei/ledidi/blob/master/docs/tutorials/Tutorial_5_-_Affinity_Catalogs.ipynb)
6. [Custom Loss Functions](https://github.com/jmschrei/ledidi/blob/master/docs/tutorials/Tutorial_6_-_Custom_Loss_Functions.ipynb)
7. [Validating Your Designs](https://github.com/jmschrei/ledidi/blob/master/docs/tutorials/Tutorial_7_-_Validating_Your_Designs.ipynb)
8. [The Ledidi Object](https://github.com/jmschrei/ledidi/blob/master/docs/tutorials/Tutorial_8_-_The_Ledidi_Object.ipynb)

### Roadmap

Ledidi is research software under active development. The broad direction, roughly in order of priority:

1. ✅ **Hardening and usability.** Expand the test suite and coverage, improve robustness and error messages, and grow the tutorials so that going from a trained model to a finished design is straightforward and hard to get wrong.

2. **Validation, trust, and benchmarking.** Gradient-based design can exploit an oracle and produce sequences that score well but are not biologically meaningful. We want first-class evaluation built in: round-tripping designs through independent held-out models, attribution-based checks that the intended motifs were actually created, realism checks against natural sequence, and a reproducible benchmark across many oracles.

3. **Interoperability and end-to-end pipelines.** Tight integration with sibling tools — such as [tangermeme](https://github.com/jmschrei/tangermeme) for attribution and in silico mutagenesis, and efficient oracle models — so that one can go from raw data all the way to finished designs using command-line tools alone.

4. **Multi-objective design under biological constraints.** Designing for several properties at once while respecting hard constraints such as GC content, restriction sites, codon usage, and the absence of off-target effects.

5. **Insertions and deletions.** Extending Ledidi beyond substitutions so that designs can change the length of a sequence, not just its content.

6. **Minimal constructs.** Merging the idea of design with that of in silico marginalization to build the smallest possible construct that still exhibits a desired property, generalizing the current greedy pruning of edits.

7. **Uncertainty and ensembling.** Confidence estimates for designs through oracle ensembles and calibration, so that both people and automated systems know when a design can be trusted.

8. **Agentic interfaces.** Programmatic, agent-facing entry points built on top of the command-line pipeline, so that an agent can carry out the full design loop end to end without a human manually performing any of the steps.

Two threads cut across all of these: **reproducibility and provenance** — capturing the full recipe behind every design so it can be audited and regenerated — and **modality breadth**, with first-class support for RNA, protein, and other categorical sequences beyond DNA.

### Citation

If you use Ledidi in your work, please cite the preprint:

> Schreiber, J., Lorbeer, F. K., Heinzl, M., Lu, Y., Stark, A., & Noble, W. S. (2025). Programmatic design and editing of cis-regulatory elements. *bioRxiv*. https://doi.org/10.1101/2025.04.22.650035

```bibtex
@article{schreiber2025ledidi,
  title={Programmatic design and editing of cis-regulatory elements},
  author={Schreiber, Jacob and Lorbeer, Franziska Katharina and Heinzl, Monika and Lu, Yang and Stark, Alexander and Noble, William Stafford},
  journal={bioRxiv},
  year={2025},
  doi={10.1101/2025.04.22.650035},
  url={https://www.biorxiv.org/content/10.1101/2025.04.22.650035v1}
}
```

### License

Ledidi is released under the [Apache 2.0 License](https://github.com/jmschrei/ledidi/blob/master/LICENSE).
