# DNA-Diffusion: 200bp regulatory elements, one checkpoint filename, and a Hydra command line

> A research codebase that generates 200bp cell type-specific synthetic regulatory elements with a diffusion model, installed through uv and configured with Hydra. The install is two commands; the friction is that training keeps the two best checkpoints while sampling expects one fixed filename.

**pinellolab/DNA-Diffusion** — 🧬 Generative modeling of regulatory DNA sequences with diffusion probabilistic models 💨 

- Repository: https://github.com/pinellolab/DNA-Diffusion
- Website: https://pinellolab.github.io/DNA-Diffusion/
- Stars: 490 · Forks: 62
- Language: Python
- License: NOASSERTION
- Published: 2026-09-10 · Updated: 2026-09-10 · Language: en
- Canonical page: https://hysenlabs.com/projects/pinellolab-dna-diffusion

## Training keeps the two best checkpoints, sampling wants one fixed filename

The default training config runs for a minimum of 2000 epochs, and the training script saves checkpoints for the lowest two validation loss values into the `checkpoints/` directory. Two, not one.

The sampling side expects a single fixed path. `configs/sampling/default.yaml` sets `checkpoint_path` to `checkpoints/model.safetensors`, and the README is explicit that if you trained the model yourself you need to make sure the checkpoint ends up at that location. Since the trainer writes by validation rank rather than by that name, closing this gap is a manual step: either rename what it produced, or point the config at the file you actually have.

Both formats work. `pt` and `safetensors` are both supported, which is why the override example in the local-checkpoint section ends in `.pt` while the default ends in `.safetensors`.

There is a smaller version of the same pattern at the data stage. The default training config uses the curated subset, and a separate debug config exists for a fast sanity run:

```bash
uv run train.py -cn train_debug
```

That config trains on a single sequence. It is the difference between a two-minute check that the pipeline wires up correctly and a 2000-epoch run.

## uv is the only install path, and uv.lock is the version record

There is one supported package manager and it is uv. Installing it on Linux:

```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```

Then the project itself:

```bash
git clone https://github.com/pinellolab/DNA-Diffusion.git
cd DNA-Diffusion
uv sync
```

`uv sync` creates a `.venv` and installs everything listed in `uv.lock`, and the README points at that lock file as the place to find exact dependency versions rather than describing them in prose. A `.python-version` file at the root and a `requires-python` of 3.12 or newer in the packaging metadata agree on the floor.

The hardware note is where the real requirement sits. The setup is compatible with both CPU and GPU, but the preferred operating system is Linux with a recent GPU, with an A100 given as the example. So the two-line install works anywhere and the intended workload does not.

There is no pip path documented and no conda instructions. For a project whose whole pitch is reproducibility, pinning the manager as tightly as the dependencies is consistent, though it does mean the lock file is mandatory reading.

## The version comes from git, and the console script is never mentioned

The packaging metadata declares the version as dynamic and builds it with a dedicated plugin. The build system requires `hatchling` plus `hatch-regex-commit`, which derives a version from the repository's own history rather than from a literal in the file. That is why there is no `version` field to read and why the three release tags are the only version record anyone outside the project will see.

The newest tags are `v0.1.1` from 2025-07-01, `v0.1.0` from 2025-06-29 and `v0.0.3` from 2025-05-05. The last push to the default branch is dated 2026-09-21, which is over a year after the most recent release.

A console script is declared and then ignored. The metadata maps a `dnadiffusion` command to a CLI entry point, yet not one documented instruction uses it. Every command in the README goes through `uv run` against a root-level script instead: `uv run train.py` for training, `uv run sample_hf.py` for sampling from the published checkpoint, `uv run sample.py` for sampling from a local file, and `uv run data/master_dataset_and_filter.py` for building the dataset.

So there are three top-level scripts plus a data script, and a fourth entry point that the documentation never reaches for.

## Hydra's dotted overrides are the command line, and quoting shifts with the count

Every runtime parameter is set through Hydra, which is why the commands look like dotted assignments rather than flags. Sampling count and batch size are the common pair:

```bash
uv run sample_hf.py sampling.guidance_scale=7.0 sampling.number_of_samples=1 sampling.sample_batch_size=1
```

Base generation runs at a guidance scale of 1.0, and `sampling.guidance_scale` moves it, so the first override in that line is a 7x change from the default rather than an arbitrary number.

Cell type selection is where the ergonomics get awkward. A single cell type needs no quoting:

```bash
uv run sample_hf.py data.cell_types=K562 sampling.number_of_samples=1 sampling.sample_batch_size=1
```

Two cell types need the value wrapped in quotes with the inner list preserved, because Hydra parses a comma-separated value as a list only when it survives as one string:

```bash
uv run sample_hf.py 'data.cell_types="K562,GM12878"' sampling.number_of_samples=1 sampling.sample_batch_size=1
```

Cell types can be given as a comma separated string or as a list. Dropping the quotes on the multi-cell form is the failure mode here, and it is the kind of thing a config layer could have normalized instead of pushing onto the user.

## Two sampling scripts, and the local path ends in .pt while the default ends in .safetensors

There are two sampling entry points and they differ in where the weights come from. `sample_hf.py` works against the checkpoint published on Hugging Face, so a plain run needs no setup at all. `sample.py` works against a file on disk, which is the one you use after training or after fetching the weights yourself.

The fetch is explicit:

```bash
wget https://huggingface.co/ssenan/DNA-Diffusion/resolve/main/model.safetensors -O checkpoints/model.safetensors
```

After that, `uv run sample.py` picks up the default location, or you override it, and note that this example uses a different extension from both the default and the wget line:

```bash
uv run sample.py sampling.checkpoint_path=checkpoints/model.pt
```

The extension difference is harmless because both formats are accepted, but it means a reader following the two sections in order downloads a `.safetensors` file and is then shown a `.pt` path.

The default output of either route is 1000 sequences per cell type, which is a large default for a smoke test. The single-sequence override exists and is shown twice, once for guidance scaling and once for cell type selection.

## The dataset is 3.59 million sequences filtered down to 12k per cell type

The published data is a small subset of the DHS Index dataset, sitting at `data/K562_hESCT0_HepG2_GM12878_12k_sequences_per_group.txt`. The filename encodes both the cell types, K562, hESC T0, HepG2 and GM12878, and the size, 12k sequences per group.

To rebuild it, one script does everything:

```bash
uv run data/master_dataset_and_filter.py
```

It downloads the source data and writes two files. `data/master_dataset.ftr` holds the full set at roughly 3.59 million sequences, and `data/filtered_dataset.txt` holds the same curated subset that ships with the repository. So the ratio between what you can download and what you are given is roughly three hundred to one.

Two details are worth noting. The full file uses a `.ftr` extension rather than the conventional Feather suffix, so tooling that pattern-matches on the usual name will not pick it up. And a rendered version of the same code is provided as a notebook under `notebooks/`, built with marimo rather than the Jupyter front end, which is also why JupyterLab ends up in the runtime dependency list.

## logs/, checkpoints/ and a lint-excluded src/refactor/ all sit in the tree

Several directories that are usually excluded are present at the top level: `checkpoints/`, `logs/`, `docs/` and `notebooks/`. Checkpoints in a repository tree is understandable for a project that publishes weights, but `logs/` is the one that raises a question, since training output is machine-generated and per-run.

The linter configuration says something about the source tree. Ruff runs on Python 3.12 with a line length of 120 and explicitly includes notebooks, which is the right call for a project where notebooks carry real logic. It also excludes a directory called `src/refactor` from linting altogether, alongside ignoring five rules by code.

The relative-import policy is strict and self-consistent. `ban-relative-imports` is set to all, and the per-file ignore list grants `tests/**/*` an exemption for the rule that governs it, with a comment saying tests may use relative imports. The same list grants tests exemptions for magic-value comparison and for assertions.

One inconsistency sits in that list. The selected rule set is only E, F and I, which are pycodestyle errors, pyflakes and isort. Two of the per-file exemptions name rule families that were never selected, so they have nothing to switch off.

## JupyterLab and Weights and Biases are required, and coverage is switched off

The dependency list is lower bounds only, with no upper bound on any of them, and two entries are heavier than you would expect from a training script. JupyterLab is a hard requirement rather than a development extra. Weights and Biases is too, which means logging is not optional in the dependency graph even if you never configure a project.

The attention implementation is also worth naming: it depends on `memory-efficient-attention-pytorch` rather than on xformers. That is a community package standing in for the official one, so the performance characteristics of the attention path are not the ones the more widely used library provides.

The rest is conventional: einops, hydra-core, matplotlib, pandas, safetensors, seaborn, torch, torchvision, tqdm and transformers, all with lower bounds.

Test configuration is where the project has clearly moved on. Pytest points at `tests` with asyncio in strict mode, but the coverage settings are commented out, including the `addopts` line that would have wired coverage into the default run and a whole block of coverage source configuration. Someone disabled it deliberately and left the commented text in place.

And the last section of the README, the examples, stops partway through its first sentence, at the words describing an example notebook.

## Conclusion

This is a research codebase with a reproducible install and unusually explicit command lines, so the friction sits in the checkpoint handoff rather than in getting it running. Two checks first. Training keeps the two lowest validation-loss checkpoints while the sampling config points at a single fixed filename, so your own run needs a manual rename or a config edit. And the required dependency list includes JupyterLab and Weights and Biases, which makes a plain training run heavier than the two-line install suggests. Also worth knowing: the package version is generated from git, the license metadata reports NOASSERTION beside a LICENSE.md file, and the newest tag is v0.1.1 from July 2025.

## FAQ

### What does DNA-Diffusion generate?

200bp cell type-specific synthetic regulatory elements, produced by a diffusion probabilistic model. The published checkpoint is hosted on Hugging Face, and the default sampling run generates 1000 sequences per cell type.

### How do I install DNA-Diffusion?

With uv. Install uv on Linux, then clone the repository, change into the directory and run `uv sync`, which creates a `.venv` and installs everything from `uv.lock`. Linux with a recent GPU such as an A100 is the preferred environment, though CPU also works.

### How do I train the DNA-Diffusion model?

Run `uv run train.py`, which uses `configs/train/default.yaml` and trains for a minimum of 2000 epochs, saving checkpoints for the two lowest validation loss values. `uv run train.py -cn train_debug` substitutes a config that trains on a single sequence.

### How do I generate sequences for one cell type with DNA-Diffusion?

Pass `data.cell_types` on the command line, for example `data.cell_types=K562`. Several cell types go in as a comma separated string or a list, and the multi-cell form needs quoting that the single-cell form does not.

### What license does DNA-Diffusion use?

The repository carries a LICENSE.md file while its license metadata reports NOASSERTION, and the page does not resolve which grant applies. Read the file in the repository rather than relying on the metadata field.

## Sources

- [Issues](https://github.com/pinellolab/DNA-Diffusion/issues)
- [pinellolab/DNA-Diffusion on GitHub](https://github.com/pinellolab/DNA-Diffusion)
- [Project website](https://pinellolab.github.io/DNA-Diffusion/)
- [README](https://github.com/pinellolab/DNA-Diffusion/blob/main/README.md)
- [Releases](https://github.com/pinellolab/DNA-Diffusion/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/pinellolab-dna-diffusion
