# smoke-bli - Small Molecule Octet/BLI Kinetics Experiment

A Python package for processing and analyzing Bio-Layer Interferometry (BLI) kinetics data acquired on Octet Red96 (Fortebio).

BLI signals are small and noisy when small molecules are investigated as ligands (analytes). This package provides tools to accurately process and fit 1:1 binding kinetics from these experiments.

**Reference:** Sun Q., Li X., et al (2020). *Nature Communications*, 11: 339. [doi:10.1038/s41467-019-14238-3](https://doi.org/10.1038/s41467-019-14238-3)

> Converted from the original R/CRAN package [`smoke`](https://cran.r-project.org/package=smoke) (v2.0.1) by Qingan Sun, Xiaojun Li, and James C Sacchettini.

## Installation

```bash
pip install pysmoke
```

### Dependencies

- numpy
- pandas
- scipy
- matplotlib
- corner

## Quick Start

```python
import pandas as pd
from smoke import Bli

# Create a Bli object
bli = Bli()

# Load your trace data (first column = time, remaining columns = sensor traces)
bli.traces = pd.read_csv("my_traces.csv", header=None)

# Set ligand concentrations (high to low) and experiment times
bli.ligand = [16.0, 8.0, 4.0, 2.0, 1.0, 0.5, 0.25]
bli.t_exp = [1260, 1860]  # [association_start, dissociation_start]

# Processing pipeline
bli.align_load(load_start=180, load_end=780)
bli.double_blank()
bli.baseline_correct(t_start=1080, t_end=1260)

# Estimate initial rates and fit global kinetics
bli.estimate()

# Option 1: Analytical kinetics fitting
bli.fit_kinetics()

# Option 2: ODE-based kinetics fitting
bli.ode_kinetics(negative_response=False)

# View results
print(bli.get_kinetics())

# Plot
bli.plot_traces()
bli.plot_kinetics()
bli.plot_residuals()
bli.plot_confidence_contours()
```

## API Reference

### `Bli` Class

#### Attributes

| Attribute | Type | Description |
|-----------|------|-------------|
| `traces` | `pd.DataFrame` | Trace data. First column is `time`, remaining columns are sensor traces. |
| `ligand` | `np.ndarray` | Ligand concentrations, ordered high to low. |
| `t_exp` | `np.ndarray` | Two-element array: `[association_start, dissociation_start]`. |
| `k_on0` | `float` or `None` | Initial on-rate estimate. |
| `k_off0` | `float` or `None` | Initial off-rate estimate. |
| `status` | `dict` | Processing step completion flags. |
| `kinetics` | `dict` | Fitted model results (populated after `fit_kinetics()` or `ode_kinetics()`). |

#### Processing Methods

| Method | Description |
|--------|-------------|
| `align_load(load_start, load_end)` | Align paired traces to the loading step. |
| `double_blank()` | Apply double-blank (double-reference) correction. |
| `baseline_correct(t_start, t_end)` | Subtract a linear baseline fitted over a time window. |
| `estimate()` | Estimate initial `k_on0` and `k_off0` from individual trace fits. |
| `fit_kinetics(negative_response=False)` | Global fit of 1:1 binding kinetics with linear drift correction. Set `negative_response=True` for decreasing signals. |
| `ode_kinetics(negative_response=True)` | ODE-based global fit of 1:1 binding kinetics with linear drift correction. |

#### Result Methods

| Method | Description |
|--------|-------------|
| `get_kinetics()` | Returns a DataFrame with KD, rMax, kOn, kOff (estimate, std error, t-value, p-value). |

#### Plotting Methods

| Method | Description |
|--------|-------------|
| `plot_traces(**kwargs)` | Plot all traces with cyan-to-magenta gradient. |
| `plot_kinetics(**kwargs)` | Plot traces with fitted kinetics overlay. |
| `plot_residuals(**kwargs)` | Plot residuals from the global fit. |
| `plot_confidence_contours()` | Corner plot showing confidence contours for kOn, kOff, and rMax. |

## R to Python Mapping

| R (original) | Python |
|--------------|--------|
| `Bli()` | `Bli()` |
| `traces(obj)` / `traces(obj)<-` | `obj.traces` |
| `ligand(obj)` / `ligand(obj)<-` | `obj.ligand` |
| `tExp(obj)` / `tExp(obj)<-` | `obj.t_exp` |
| `kOn0(obj)` / `kOn0(obj)<-` | `obj.k_on0` |
| `kOff0(obj)` / `kOff0(obj)<-` | `obj.k_off0` |
| `status(obj)` | `obj.status` |
| `alignLoad(obj, loadStart, loadEnd)` | `obj.align_load(load_start, load_end)` |
| `doubleBlank(obj)` | `obj.double_blank()` |
| `baseline(obj, tStart, tEnd)` | `obj.baseline_correct(t_start, t_end)` |
| `estimate(obj)` | `obj.estimate()` |
| `fitKinetics(obj)` | `obj.fit_kinetics()` |
| `odeKinetics(obj)` | `obj.ode_kinetics()` |
| `kinetics(obj)` | `obj.get_kinetics()` |
| `plotTraces(obj)` | `obj.plot_traces()` |
| `plotKinetics(obj)` | `obj.plot_kinetics()` |
| `plotResiduals(obj)` | `obj.plot_residuals()` |
| `plotConfidenceContours(obj)` | `obj.plot_confidence_contours()` |

## License

GPL-3.0-or-later
