Metadata-Version: 2.4
Name: nuc2d
Version: 2.0.0
Summary: Nuc2D visualizes RNA and DNA secondary structures as publication-ready SVG images.
Author: Soma Ishii
License-Expression: MIT
Project-URL: Repository, https://github.com/Soma-yu/nuc2d
Project-URL: Changelog, https://github.com/Soma-yu/nuc2d/releases
Keywords: nucleic acid,RNA,DNA,secondary structure,visualization
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Science/Research
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
Classifier: Topic :: Scientific/Engineering :: Visualization
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Operating System :: OS Independent
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.24.0
Requires-Dist: matplotlib>=3.6.0
Requires-Dist: svgwrite<2,>=1.4
Requires-Dist: fonttools>=4.40.0
Dynamic: license-file

# Nuc2D: Visualize RNA and DNA secondary structures

The output is SVG, so a figure stays sharp at any size and remains editable in
tools such as Illustrator or Inkscape: it can be adjusted without being
redrawn.

<p align="center">
  <img src="https://raw.githubusercontent.com/Soma-yu/nuc2d/main/docs/images/example.png" width="100%">
</p>

One structure, drawn twice: on its own, and with its sequences and the
probability of each nucleotide being in the state the structure puts it in.

[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/Soma-yu/nuc2d/blob/main/examples/nuc2d_intro.ipynb)

An introductory notebook, written in Japanese. It runs in Google Colab, so
there is nothing to set up on your own machine.

## Installation

```bash
pip install nuc2d
```

## Quick start

```python
from nuc2d import draw_svg

# A tRNA cloverleaf, split into two strands.
CLOVERLEAF = "(((((((..((((........)))).(((((.......+))))).....(((((.......))))))))))))...."

scene = draw_svg(CLOVERLEAF)

scene.save_svg("output.svg")
```

<p align="center">
  <img src="https://raw.githubusercontent.com/Soma-yu/nuc2d/main/docs/images/structure.png" width="55%">
</p>

Structures are written with `(` and `)` for the two halves of a base pair,
`.` for an unpaired nucleotide, and `+` for a break between strands. An arrow
marks each 3' terminus.

In Jupyter, a scene that ends a cell is displayed as it is:

```python
scene
```

Anywhere else in a cell, `display(scene)` from `IPython.display` shows it.

An input that is not a well-formed structure raises `ParseError`, a subclass
of `ValueError`:

```python
from nuc2d import ParseError

try:
    draw_svg("(((")
except ParseError as error:
    print(error)
```

## Sequence annotation

Nucleotide sequences can be provided through the `sequences` argument, one per
strand, in the order the strands appear in the structure.

```python
SEQUENCES = [
    "GCGGAUUUAGCUCAGUUGGGAGAGCGCCAGACUGAAGA",
    "UCUGGAGGUCCUGUGUUCGAUCCACAGAAUUCGCACCA",
]

scene = draw_svg(CLOVERLEAF, sequences=SEQUENCES)
```

<p align="center">
  <img src="https://raw.githubusercontent.com/Soma-yu/nuc2d/main/docs/images/sequences.png" width="55%">
</p>

A wrong number of sequences, or a sequence longer or shorter than its strand,
raises `ValueError` rather than drawing something misleading.

## Base-pair probability visualization

Base-pair probabilities are visualized by passing a probability matrix
through the `basepair_probabilities` argument. A colorbar is placed beside the
structure.

Element `[i][j]` of the matrix is how likely nucleotides `i` and `j` are to be
paired with each other. Only the elements above the diagonal are read, so the
matrix may be symmetric, as the one below is, or have only its upper triangle
filled in. A real matrix comes from a structure prediction tool; the one below
is made up, which is enough to see what the drawing does.

```python
import numpy as np

flat = CLOVERLEAF.replace("+", "")
probs = np.zeros((len(flat), len(flat)))

stack = []
for i, char in enumerate(flat):
    if char == "(":
        stack.append(i)
    elif char == ")":
        left = stack.pop()
        # Made up: the acceptor and anticodon stems are the certain ones.
        probs[left][i] = probs[i][left] = 0.9 if left < 8 or 25 < left < 32 else 0.45

scene = draw_svg(
    CLOVERLEAF,
    sequences=SEQUENCES,
    basepair_probabilities=probs,
    colorbar_label="Base-pair probability",
)
```

<p align="center">
  <img src="https://raw.githubusercontent.com/Soma-yu/nuc2d/main/docs/images/probabilities.png" width="65%">
</p>

Each nucleotide is colored by the probability of the state the structure puts
it in: of pairing with its partner if it is paired, and of being unpaired if it
is not. A probability below 0 or above 1, as rounding can leave one, is shown
in the color of 0 or of 1. A NaN raises `ValueError`, since there is no color
to show it in.

The `colorbar_label` argument labels the colorbar. The `colormap` argument sets
the colors: a matplotlib colormap itself, such as `mpl.colormaps["turbo"]`, the
default, rather than its name.

## Title

The `title` argument writes one line above the structure and its colorbar,
centered over them:

```python
scene = draw_svg(CLOVERLEAF, title="tRNA")
```

## Output size

```python
# One of the two: the other follows from the aspect ratio of the structure.
scene = draw_svg(CLOVERLEAF, width_px=600)

# Both: used as written.
scene = draw_svg(CLOVERLEAF, width_px=600, height_px=600)
```

Giving neither `width_px` nor `height_px` draws the structure at its own
size. Giving both keeps the structure's proportions and centers it, rather than
stretching it to fit.

## Layout and style

`RadialLayoutEngine`, passed as the `layout_engine` argument, controls
geometry — how far apart nucleotides are placed. `StructureStyle`, passed as
the `style` argument, controls appearance — colors, stroke widths, nucleotide
size and letter size.

```python
import matplotlib as mpl

from nuc2d import RadialLayoutEngine, StructureStyle

scene = draw_svg(
    CLOVERLEAF,
    sequences=SEQUENCES,
    basepair_probabilities=probs,
    colormap=mpl.colormaps["viridis"],
    layout_engine=RadialLayoutEngine(
        stem_spacing=18.0,
        loop_spacing=24.0,
    ),
    style=StructureStyle(
        backbone_color="#333333",
        basepair_color="crimson",
        nucleotide_radius=5.0,
    ),
    colorbar_label="Base-pair probability",
)
```

<p align="center">
  <img src="https://raw.githubusercontent.com/Soma-yu/nuc2d/main/docs/images/styling.png" width="65%">
</p>

Colors are written as SVG writes them: a name such as `black`, `#rrggbb`, or
`none`. Dash patterns are `none` or lengths separated by commas, such as
`1,1`. Spacings are positive, and sizes are numbers of at least 0.

Anything else raises an exception as soon as it is set, rather than when
something is drawn with the engine or the style. So does a misspelled setting,
such as `style.nucleotide_colour = "black"`, which would otherwise be ignored.

## Combining several structures

`draw_svg` draws one structure and frames it as a `Scene`. To put several
structures in one picture, draw each as a component instead, place the
components, and frame the result:

- `draw_svg_as_component` takes the same arguments as `draw_svg`, less the
  size, and returns the structure, with its title and its colorbar if it has
  them, as a `Component`. `draw_svg(...)` is equivalent to
  `Scene(draw_svg_as_component(...))`.
- `Placement` says where a component goes and at what size, and
  `Component.from_placements` makes one component of several placed ones.
- `Scene` frames a component, and is what is saved or shown.

This is how the picture at the top of this page is drawn:

```python
from nuc2d import Component, Placement, Scene, draw_svg_as_component

components = [
    # The structure on its own.
    draw_svg_as_component(CLOVERLEAF),
    # The same structure, with its sequences and base-pair probabilities.
    draw_svg_as_component(
        CLOVERLEAF,
        sequences=SEQUENCES,
        basepair_probabilities=probs,
        colorbar_label="Base-pair probability",
    ),
]

# Lay the components out in a row, all as tall as the first.
height = components[0].bbox.height
placements, cursor_x = [], 0.0
for component in components:
    placement = Placement(
        component, x=cursor_x, scale=height / component.bbox.height
    )
    placements.append(placement)
    cursor_x = placement.bbox.xmax

Scene(Component.from_placements(placements)).save_svg("panel.svg")
```

<p align="center">
  <img src="https://raw.githubusercontent.com/Soma-yu/nuc2d/main/docs/images/example.png" width="100%">
</p>

`Placement` puts one point of a component at the point its `x` and `y`
arguments give, and scales the component about that point by its `scale`
argument. The point is the upper left corner of the component's box unless the
`anchor` argument names another: one of `"upper left"`, `"upper center"`,
`"upper right"`, `"center left"`, `"center"`, `"center right"`, `"lower left"`,
`"lower center"` and `"lower right"`, or a tuple of two fractions of the box's
width and height, each from 0 to 1, such as `(0.5, 0.0)` for the middle of its
top edge.

## Versioning

Nuc2D follows [Semantic Versioning](https://semver.org/). What a version
promises is the public API: the names the package exports, the arguments they
take, and the type of exception each raises for the mistakes its docstring
describes. Those change only in a major release. A minor release may raise a
subclass of that type instead, or accept what was refused before. The text of a
repr or of an error message is not part of the promise, and neither are
warnings.

Everything public is imported from `nuc2d` itself. The modules inside the
package all begin with an underscore, such as `nuc2d._svg`: they are where the
code lives, and they can change in any release.

The classes are not meant to be subclassed. A type checker reports a subclass
of one, and what a subclass would rely on is not part of the promise. Nor is
pickling: what an object holds is private, so one pickled under one version of
nuc2d may not load under another. Some of the classes are dataclasses, but that
is not promised either: what `dataclasses.fields`, `asdict`, `astuple` and
`replace` do with them may change, and so may an attribute read from a class
rather than from an instance, such as `StructureStyle.nucleotide_radius`.

A release that drops a version of Python which has reached its end of life,
or raises the oldest version of a dependency that it supports, is a minor
release. pip reads which versions of Python a release supports, so an
environment with an older Python keeps installing the last release that
supported it.

The drawing is not part of that promise. A minor release may place a
nucleotide differently, enclose a structure more tightly, draw a figure at
another size, write the same shape as different SVG, or change a default that
sets how a figure looks: a value `StructureStyle` or `RadialLayoutEngine`
starts with, or the colormap used when none is given. A figure regenerated
under a newer version can then come out different, and so can the box of a
component, and the size of a scene given neither `width_px` nor `height_px`.
Text is measured with the font installed on the machine,
so a drawing can differ between two machines running the same version as
well. An SVG already saved to disk is of course unaffected.

## Changes

Release notes for every version are on the
[releases page](https://github.com/Soma-yu/nuc2d/releases).

## License

This project is licensed under the MIT License.
