Metadata-Version: 2.4
Name: SBMLLayout
Version: 0.5.0
Summary: Clean API for reading and writing the SBML Layout extension
Author: Herbert Sauro
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
Requires-Python: >=3.8
Description-Content-Type: text/markdown
Requires-Dist: python-libsbml
Requires-Dist: skia-python
Dynamic: requires-python

# SBMLLayout

A clean Python API for attaching, editing, and rendering the
[SBML Layout extension](https://sbml.org/documents/specifications/level-3/version-1/layout/).

## Installation

```bash
pip install SBMLLayout
```

## Why?

Existing tools (SBMLDiagrams, SBMLNetwork) are primarily *visualisation* tools
that happen to expose a layout API as a side-effect. When you need to attach or
manipulate layout geometry programmatically — especially with alias (repeated)
species nodes or full bezier curves — you quickly hit undocumented requirements
of the underlying libsbml Layout package. SBMLLayout takes care of all of that
bookkeeping for you and includes its own skia-based renderer with full bezier
support.

## Quick start

```python
import tellurium as te
from SBMLLayout import Layout

r = te.loada('''
    J1: S1 -> S2; k1*S1
    k1 = 0.1; S1 = 10; S2 = 0
''')

layout = Layout(r.getSBML())
layout.addNode('S1', x=100, y=150, w=60, h=40)
layout.addNode('S2', x=300, y=150, w=60, h=40)
layout.addReaction('J1', center=(200, 170),
                   reactants=['S1'], products=['S2'])

layout.draw()                        # display inline in Jupyter / IDE
layout.draw(output='network.png')    # save PNG  (scale=2 for high-DPI)
layout.draw(output='network.pdf')    # save PDF
layout.draw(output='network.svg')    # save SVG
```

## Alias nodes

Species that appear more than once in a diagram (alias nodes) are
supported as a first-class concept. Call `addNode` multiple times with
the same species id and `alias=True` for each extra copy. Copy indices
are assigned in call order (first call = copy 0, second = copy 1, …).
Use `'A:1'` syntax in reaction lists to route arms to a specific copy.

```python
layout.addNode('A',  x=60,  y=200, w=50, h=40)             # prime  (copy 0)
layout.addNode('A',  x=280, y=300, w=50, h=40, alias=True)  # alias  (copy 1)

layout.addReaction('J1', center=(184, 205),
                   reactants=['S1'], products=['S2', 'A'])      # 'A' = copy 0
layout.addReaction('J2', center=(185, 346),
                   reactants=['S2', 'A:1'], products=['S3'])    # 'A:1' = copy 1
```

## Bezier curves

Pass a `handles` list to `addReaction` to specify cubic bezier control
points for each arm. Each entry is a pair of tuples
`((BP1x, BP1y), (BP2x, BP2y))` where BP1 is near the species node and
BP2 is near the junction. Use `None` for a straight-line arm.

```python
# Bi-uni reaction: S1 + S2 -> S3 with curved reactant arms
layout.addReaction('J1',
    reactants=['S1', 'S2'],
    products=['S3'],
    center=(250, 203),
    handles=[
        ((210, 102), (240, 195)),  # S1 arm: (BP1, BP2)
        ((210, 302), (240, 211)),  # S2 arm: (BP1, BP2)
        None,                       # S3 arm: straight line
    ])
```

## Direct reactions

For uni-uni reactions with no junction node, use `direct=True`. The line
connects the reactant directly to the product with no junction circle.

```python
# Straight direct line
layout.addReaction('J1', reactants=['S1'], products=['S2'], direct=True)

# Bezier direct curve
layout.addReaction('J1', reactants=['S1'], products=['S2'],
    direct=True,
    handles=[((160, 100), (240, 200))])  # single (BP1, BP2) pair
```

## Compartments

```python
layout.addCompartment('cell', x=10, y=10, w=500, h=400)
```

## Method chaining

All `add*` and `set*` methods return `self`, so calls can be chained:

```python
layout = (Layout(r.getSBML())
    .addNode('S1', x=100, y=150, w=60, h=40)
    .addNode('S2', x=300, y=150, w=60, h=40)
    .addReaction('J1', center=(200, 170),
                 reactants=['S1'], products=['S2'])
    .setReactionStyle('J1', color=(0, 100, 200, 255))
    .setNodeStyle('S1', fill=(220, 255, 220, 255)))
layout.draw()
```

## Using the SBML with other tools

`toSBML()` returns a valid SBML Level 3 string that can be loaded into
any tool supporting the SBML Layout package:

```python
sbml = layout.toSBML()

# SBMLDiagrams
import SBMLDiagrams
df = SBMLDiagrams.load(sbml)
df.draw()

# Or save for use elsewhere
with open('model_with_layout.xml', 'w') as f:
    f.write(sbml)
```

Note that per-element style overrides set via `setReactionStyle` and
`setNodeStyle` are stored on the `Layout` object and are used by
`layout.draw()`. They are not written into the SBML and will not carry
over if the SBML is loaded by another tool.

## Global styling

Pass a `Style` instance to `draw()` to customise the appearance of the
entire diagram. All colours are `(R, G, B, A)` tuples with values 0–255.

```python
from SBMLLayout import Layout, Style

s = Style()
s.node_fill        = (200, 230, 255, 255)   # light blue fill
s.node_stroke      = (0,   80,  160, 255)   # dark blue border
s.reaction_stroke  = (50,  50,  50,  255)   # dark grey lines
s.node_gap         = 10.0                   # gap between node edge and line
s.junction_visible = False                  # hide junction circles

layout.draw(output='network.png', scale=2, style=s)
```

### Full Style reference

| Attribute | Default | Description |
|-----------|---------|-------------|
| **Canvas** | | |
| `background_color` | `(255,255,255,255)` | Canvas background colour |
| `margin` | `30.0` | Padding around the network in pixels |
| **Compartments** | | |
| `compartment_fill` | `(240,240,255,180)` | Compartment fill colour |
| `compartment_stroke` | `(100,100,200,255)` | Compartment border colour |
| `compartment_stroke_width` | `2.0` | Compartment border width |
| `compartment_corner_radius` | `8.0` | Compartment rounded corner radius |
| **Species nodes** | | |
| `node_fill` | `(250,213,211,255)` | Node fill colour |
| `node_stroke` | `(130,37,31,255)` | Node border colour |
| `node_stroke_width` | `2.0` | Node border width |
| `node_corner_radius` | `6.0` | Node rounded corner radius |
| **Labels** | | |
| `label_color` | `(0,0,0,255)` | Text colour |
| `label_font_size` | `14.0` | Font size in points |
| `label_font_family` | `'Arial'` | Font family name |
| **Reaction lines** | | |
| `reaction_stroke` | `(0,0,0,255)` | Reaction line colour |
| `reaction_stroke_width` | `2.0` | Reaction line width |
| `node_gap` | `6.0` | Gap between node edge and start/end of reaction arms. Set to `0.0` for lines that touch the node edge exactly. Applies uniformly to reactant and product ends on all reaction styles. |
| **Junction circle** | | |
| `junction_fill` | `(130,37,31,255)` | Junction circle fill colour |
| `junction_radius` | `5.0` | Junction circle radius |
| `junction_visible` | `True` | Whether to draw the junction circle |
| **Arrowhead** | | |
| `arrow_fill` | `(0,0,0,255)` | Arrowhead fill colour |
| `arrow_length` | `12.0` | Arrowhead length (tip to base) in pixels |
| `arrow_width` | `7.0` | Arrowhead width at base in pixels |

## Per-element styling

Individual reactions and nodes can be styled independently using
`setReactionStyle` and `setNodeStyle`. These override the corresponding
global `Style` values for the named element only.

```python
# Highlight a specific reaction in red with a thicker line
layout.setReactionStyle('J4', color=(200, 0, 0, 255), stroke_width=3.0)

# Change just the colour of another reaction
layout.setReactionStyle('J1', color=(0, 100, 200, 255))

# Style a specific node
layout.setNodeStyle('E', fill=(255, 230, 200, 255), stroke=(180, 80, 0, 255))

# Style a node including its label colour
layout.setNodeStyle('A', fill=(220, 255, 220, 255), stroke=(0, 120, 0, 255),
                    label_color=(0, 80, 0, 255))
```

When a reaction colour is set via `setReactionStyle`, it applies to the
reaction line, arrowhead, and junction circle together so everything stays
visually consistent.

## API reference

### `Layout(sbml_string)`

Create a new layout for the given SBML Level 3 model string.
Raises `ValueError` if the SBML is invalid or contains no model.

### `addNode(species_id, x, y, w=60, h=30, alias=False)`

Add a species node. `x, y` are the top-left corner of the bounding box.

### `addReaction(reaction_id, reactants, products, center=None, handles=None, direct=False)`

Add a reaction.

| Parameter | Description |
|-----------|-------------|
| `reactants` / `products` | Lists of species refs. `'S1'` = copy 0; `'A:1'` = copy 1 of A. |
| `center` | `(x, y)` junction point. Omit to let the renderer compute a default. |
| `handles` | One `((BP1x,BP1y),(BP2x,BP2y))` pair per arm (reactants first, then products), or `None` for a straight arm. Omit entirely for all-straight lines. |
| `direct` | If `True`, draw a single line/curve from reactant to product with no junction. For a direct bezier supply one handle pair. |

### `addCompartment(compartment_id, x, y, w, h)`

Add a compartment bounding box.

### `setReactionStyle(reaction_id, color=None, stroke_width=None)`

Override the visual style for a single reaction. Returns `self`.

| Parameter | Description |
|-----------|-------------|
| `reaction_id` | Must match a reaction id passed to `addReaction`. |
| `color` | `(R, G, B, A)` tuple. Overrides the line, arrowhead, and junction circle colour for this reaction. |
| `stroke_width` | Float. Overrides the line thickness for this reaction. |

### `setNodeStyle(species_id, fill=None, stroke=None, stroke_width=None, label_color=None)`

Override the visual style for a single species node. Applies to all
copies (prime and aliases) of that species. Returns `self`.

| Parameter | Description |
|-----------|-------------|
| `species_id` | Must match a species id passed to `addNode`. |
| `fill` | `(R, G, B, A)` tuple. Overrides the node fill colour. |
| `stroke` | `(R, G, B, A)` tuple. Overrides the node border colour. |
| `stroke_width` | Float. Overrides the node border thickness. |
| `label_color` | `(R, G, B, A)` tuple. Overrides the label text colour. |

### `draw(output=None, scale=1.0, style=None)`

Render the network. If `output` is omitted, displays inline (Jupyter /
opens in default image viewer). Supported extensions: `.png`, `.pdf`,
`.svg`. Per-element styles set via `setReactionStyle` and `setNodeStyle`
are always applied regardless of whether a `Style` instance is passed.

### `toSBML()`

Return a valid SBML Level 3 string with the Layout package embedded.
Per-element styles are not included in the SBML output.

## Compatibility

The SBML produced by `toSBML()` is compatible with:
- [SBMLDiagrams](https://github.com/sys-bio/SBMLDiagrams)
- [SBMLNetwork](https://github.com/sys-bio/SBMLNetwork)
- Any tool supporting SBML Level 3 Layout package v1

## Note on bezier curves and SBMLDiagrams

SBMLDiagrams does not read bezier curve data from the SBML Layout
extension — it computes its own handle positions after loading. If you
need bezier curves with SBMLDiagrams, use its `setReactionBezierHandles`
API after `SBMLDiagrams.load()`. The SBMLLayout renderer reads and draws
the full two-control-point cubic bezier data correctly.
