Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,19 @@ jobs:
fail-fast: false
matrix:
version:
- '1.6'
- '1'
- 'nightly'
os:
- ubuntu-latest
arch:
- x64
steps:
- uses: actions/checkout@v2
- uses: julia-actions/setup-julia@v1
- uses: actions/checkout@v4
- uses: julia-actions/setup-julia@v2
with:
version: ${{ matrix.version }}
arch: ${{ matrix.arch }}
- uses: actions/cache@v1
- uses: actions/cache@v4
env:
cache-name: cache-artifacts
with:
Expand All @@ -35,15 +35,15 @@ jobs:
- uses: julia-actions/julia-buildpkg@v1
- uses: julia-actions/julia-runtest@v1
- uses: julia-actions/julia-processcoverage@v1
- uses: codecov/codecov-action@v1
- uses: codecov/codecov-action@v4
with:
file: lcov.info
docs:
name: Documentation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: julia-actions/setup-julia@v1
- uses: actions/checkout@v4
- uses: julia-actions/setup-julia@v2
with:
version: '1'
- run: |
Expand Down
72 changes: 72 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What This Package Does

SMLMFrameConnection performs frame-connection on 2D SMLM localization data: combining repeated localizations of a single blinking fluorophore across frames into a single higher-precision localization. Implements the spatiotemporal LAP algorithm from Schodt & Lidke (2021).

## Development Commands

```bash
# Run tests
julia --project=. -e 'using Pkg; Pkg.test()'

# Run a single test file
julia --project=. -e 'using Test, SMLMFrameConnection, SMLMData; include("test/test_helpers.jl"); include("test/test_frameconnect.jl")'

# Build docs locally
julia --project=docs -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()'
julia --project=docs docs/make.jl

# Quick REPL usage
julia --project=.
```

Test files must `include("test/test_helpers.jl")` first since `runtests.jl` loads shared fixtures from it.

## Architecture

### Algorithm Pipeline

`frameconnect()` in `frameconnect.jl` is the main entry point. It orchestrates a 4-stage pipeline:

1. **Precluster** (`precluster.jl`): Spatiotemporal clustering using KDTree nearest-neighbor search. Groups localizations within `max_sigma_dist * mean(σ)` distance and `max_frame_gap` frames. Stores cluster assignments in emitter `track_id` fields.

2. **Estimate parameters** (`estimateparams.jl`, `estimatedensities.jl`): Estimates photophysics rates (k_on, k_off, k_bleach, p_miss) from precluster statistics. Uses Optim.jl NelderMead to fit cumulative localization counts. `estimatedensities.jl` estimates local emitter density per cluster using KDTree neighbor distances.

3. **Connect via LAP** (`connectlocalizations.jl` -> `create_costmatrix.jl` -> `solveLAP.jl` -> `linkclusters.jl`): For each multi-emitter precluster, builds a 2Nx2N cost matrix with connection/birth/death blocks using negative log-likelihoods from spatial separation, observation probability, and photophysics. Solves with Hungarian.jl. `linkclusters.jl` updates `connectID` from LAP assignments.

4. **Combine** (`combinelocalizations.jl`): MLE weighted mean using full 2x2 covariance (precision-weighted). Produces higher-precision output localizations.

### Data Flow

- Input/output: `BasicSMLD` from SMLMData.jl containing `Emitter2DFit` emitters
- Internal representation: `organizeclusters()` converts to `Vector{Matrix{Float32}}` where each matrix is a cluster with columns: `[x, y, σ_x, σ_y, σ_xy, frame, dataset, connectID, sortindex]`
- `ParamStruct`: mutable struct accumulating estimated parameters through the pipeline
- Return: tuple `(combined::BasicSMLD, info::FrameConnectInfo)`

### Key Types

- `FrameConnectConfig <: AbstractSMLMConfig`: User-facing config (keyword-constructible)
- `FrameConnectInfo{T} <: AbstractSMLMInfo`: Algorithm output metadata (track assignments, rates, timing)
- `ParamStruct`: Internal mutable state for pipeline parameters (not exported for direct use)

### Dual API Pattern

`frameconnect()` accepts both kwargs and a `FrameConnectConfig` struct. The kwargs form constructs the config internally. Both `FrameConnectConfig` and `FrameConnectInfo` inherit from SMLMData abstract types.

## Dependencies

- **SMLMData.jl** (v0.7): Provides `BasicSMLD`, `Emitter2DFit`, `IdealCamera`, abstract config/info types
- **Hungarian.jl**: LAP solver in `solveLAP.jl`
- **NearestNeighbors.jl**: KDTree for preclustering and density estimation
- **Optim.jl**: Parameter estimation (Fminbox + NelderMead)

## Conventions

- Positions and uncertainties in microns
- Frame numbers are 1-based integers
- `track_id=0` means unconnected; populated values are compressed to `1:n_tracks`
- Mutating functions use `!` suffix with non-mutating wrappers (e.g., `connectlocalizations!`/`connectlocalizations`)
- Emitter precision type `ET` is derived from emitter field values, not SMLD type parameter
4 changes: 2 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "SMLMFrameConnection"
uuid = "1517b1a9-81e8-461f-b994-92eb29599690"
authors = ["klidke@unm.edu"]
version = "0.3.0"
version = "0.5.0"

[deps]
Hungarian = "e91730f6-4275-51fb-a7a0-7064cfbd3b39"
Expand All @@ -15,7 +15,7 @@ StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91"
Hungarian = "0.6, 0.7"
NearestNeighbors = "0.4"
Optim = "1, 2"
SMLMData = "0.6"
SMLMData = "0.7"
StatsBase = "0.33, 0.34"
julia = "1.6"

Expand Down
172 changes: 86 additions & 86 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,7 @@
[![Build Status](https://github.com/JuliaSMLM/SMLMFrameConnection.jl/workflows/CI/badge.svg)](https://github.com/JuliaSMLM/SMLMFrameConnection.jl/actions)
[![Coverage](https://codecov.io/gh/JuliaSMLM/SMLMFrameConnection.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/JuliaSMLM/SMLMFrameConnection.jl)

## Overview

SMLMFrameConnection performs **frame-connection** on 2D localization microscopy data: combining repeated localizations of a single blinking fluorophore into a single higher-precision localization.

Uses the spatiotemporal clustering algorithm from [Schodt & Lidke 2021](https://doi.org/10.3389/fbinf.2021.724325).
Frame-connection for single-molecule localization microscopy: linking localizations from the same fluorophore blinking event across consecutive frames into single, higher-precision localizations. Uses spatiotemporal LAP assignment to optimally connect temporally adjacent detections based on spatial proximity and estimated blinking kinetics.

## Installation

Expand All @@ -21,124 +17,128 @@ Pkg.add("SMLMFrameConnection")
## Quick Start

```julia
using SMLMData, SMLMFrameConnection
using SMLMFrameConnection

# Frame connection on localization data
(combined, info) = frameconnect(smld)

# Output: combined high-precision localizations
println("$(info.n_input) → $(info.n_combined) localizations")
```

For complete SMLM workflows (detection + fitting + frame-connection + rendering), see [SMLMAnalysis.jl](https://github.com/JuliaSMLM/SMLMAnalysis.jl).

## Configuration

`frameconnect()` accepts keyword arguments or a `FrameConnectConfig` struct:

| Parameter | Default | Description |
|-----------|---------|-------------|
| `n_density_neighbors` | 2 | Nearest preclusters for local density estimation |
| `max_sigma_dist` | 5.0 | Sigma multiplier for preclustering distance threshold |
| `max_frame_gap` | 5 | Maximum frame gap for temporal adjacency |
| `max_neighbors` | 2 | Maximum nearest-neighbors for precluster membership |

# Run frame connection on your data
smld_connected, smld_preclustered, smld_combined, params = frameconnect(smld)
```julia
# Keyword form (most common)
(combined, info) = frameconnect(smld; max_frame_gap=10, max_sigma_dist=3.0)

# smld_combined is the main output - higher precision localizations
# Config struct form (reusable settings)
config = FrameConnectConfig(max_frame_gap=10, max_sigma_dist=3.0)
(combined, info) = frameconnect(smld, config)
```

## Input Requirements
**Parameter guidance:** Default values work well for standard dSTORM/PALM data. For dense samples, reduce `max_sigma_dist` to 3.0. For long dark states (dSTORM), increase `max_frame_gap` to 10-20.

## Output Format

`frameconnect()` returns `(combined::BasicSMLD, info::FrameConnectInfo)`.

Input must be a `BasicSMLD{T, Emitter2DFit{T}}` from [SMLMData.jl](https://github.com/JuliaSMLM/SMLMData.jl).
| Output | Description |
|--------|-------------|
| `combined` | High-precision combined localizations (main output) |
| `info.connected` | Input with `track_id` assigned (per-frame data with labels) |
| `info.n_input` | Number of input localizations |
| `info.n_tracks` | Number of tracks formed |
| `info.n_combined` | Number of output localizations |
| `info.k_on` | Estimated on rate (1/frame) |
| `info.k_off` | Estimated off rate (1/frame) |
| `info.k_bleach` | Estimated bleach rate (1/frame) |
| `info.p_miss` | Probability of missed detection |
| `info.initial_density` | Density estimate per cluster (emitters/μm²) |
| `info.elapsed_s` | Wall time (seconds) |
| `info.algorithm` | Algorithm used (`:lap`) |
| `info.n_preclusters` | Number of preclusters formed |

**Required fields** (algorithm fails without these):
- `x`, `y`: Position coordinates in microns
- `σ_x`, `σ_y`: Position uncertainties in microns (used for MLE weighting; must be > 0)
- `frame`: Frame number (1-based integer)
### Combination Method

**Optional fields** (combined in output if present):
- `photons`, `σ_photons`: Photon count and uncertainty (summed across connected localizations)
- `bg`, `σ_bg`: Background and uncertainty
- `dataset`: Dataset identifier (defaults to 1; for multi-ROI or multi-acquisition data)
Connected localizations are combined using maximum likelihood estimation (MLE) weighted mean:
- Position: `x_combined = Σ(x/σ²) / Σ(1/σ²)` (inverse-variance weighted)
- Uncertainty: `σ_combined = √(1/Σ(1/σ²))` ≈ `σ_individual / √n`
- Photons: summed across connected localizations

## Complete Example
## Algorithm Pipeline

1. **Precluster**: Spatiotemporal clustering using KDTree nearest-neighbor search within `max_sigma_dist * σ` distance and `max_frame_gap` frames
2. **Estimate parameters**: Fit photophysics rates (k_on, k_off, k_bleach, p_miss) from precluster statistics using Optim.jl NelderMead
3. **Connect via LAP**: Build cost matrix from spatial separation and photophysics likelihoods, solve with Hungarian.jl
4. **Combine**: MLE weighted mean using full 2×2 covariance (precision-weighted)

## Example

```julia
using SMLMData, SMLMFrameConnection
using SMLMFrameConnection

# Create camera (pixel_ranges, pixel_size in μm)
cam = IdealCamera(1:512, 1:512, 0.1)

# Create emitters representing the same molecule blinking across 3 frames
# Constructor: Emitter2DFit{T}(x, y, photons, bg, σ_x, σ_y, σ_photons, σ_bg; frame, dataset, track_id)
emitters = [
Emitter2DFit{Float64}(
5.0, 5.0, # x, y position (μm)
5.00, 5.00, # x, y position (μm)
1000.0, 10.0, # photons, background
0.02, 0.02, # σ_x, σ_y uncertainties (μm)
50.0, 2.0; # σ_photons, σ_bg
frame=1
0.02, 0.02, 0.0, # σ_x, σ_y, σ_xy
50.0, 2.0, # σ_photons, σ_bg
1, 1, 0, 1 # frame, dataset, track_id, id
),
Emitter2DFit{Float64}(5.01, 5.01, 1200.0, 12.0, 0.02, 0.02, 60.0, 2.0; frame=2),
Emitter2DFit{Float64}(5.02, 4.99, 1100.0, 11.0, 0.02, 0.02, 55.0, 2.0; frame=3),
Emitter2DFit{Float64}(5.01, 5.01, 1200.0, 12.0, 0.02, 0.02, 0.0, 60.0, 2.0, 2, 1, 0, 2),
Emitter2DFit{Float64}(5.02, 4.99, 1100.0, 11.0, 0.02, 0.02, 0.0, 55.0, 2.0, 3, 1, 0, 3),
]

# Create SMLD: BasicSMLD(emitters, camera, n_frames, n_datasets)
smld = BasicSMLD(emitters, cam, 3, 1)

# Run frame connection
_, _, smld_combined, _ = frameconnect(smld)

# Result: localizations connected based on spatial/temporal proximity
# Combined uncertainty: σ_combined ≈ σ_individual / √n_connected
```
(combined, info) = frameconnect(smld)

## Outputs Explained

```julia
smld_connected, smld_preclustered, smld_combined, params = frameconnect(smld)
# Result: Combined uncertainty ≈ σ_individual / √n_connected
println("$(info.n_input) → $(info.n_combined) localizations in $(info.elapsed_s)s")
```

| Output | Description | When to use |
|--------|-------------|-------------|
| `smld_combined` | **Main output.** Combined high-precision localizations | Standard analysis |
| `smld_connected` | Original localizations with `track_id` populated | When you need per-frame data with connection labels |
| `smld_preclustered` | Intermediate preclustering result | Debugging, algorithm tuning |
| `params` | Estimated photophysics + input parameters | Inspecting estimated k_on, k_off, density |

## Parameters

```julia
frameconnect(smld;
nnearestclusters = 2, # Clusters used for local density estimation
nsigmadev = 5.0, # Distance threshold = nsigmadev × localization uncertainty
maxframegap = 5, # Max frames between connected localizations
nmaxnn = 2 # Nearest neighbors checked during preclustering
)
```

**Parameter guidance:**
- `nsigmadev`: Higher values allow connections over larger distances. Default (5.0) works for typical SMLM data. Reduce for dense samples.
- `maxframegap`: Set based on expected blinking duration. For dSTORM with long dark states, increase to 10-20.
- Defaults work well for standard dSTORM/PALM data with typical blinking kinetics.

## Estimated Parameters (ParamStruct)

The algorithm estimates fluorophore photophysics from your data:

| Field | Description |
|-------|-------------|
| `k_on` | Rate of transitioning from dark to visible state (1/frame) |
| `k_off` | Rate of transitioning from visible to dark state (1/frame) |
| `k_bleach` | Photobleaching rate (1/frame) |
| `p_miss` | Probability of missing a localization when fluorophore is on |
| `initialdensity` | Estimated emitter density per cluster (emitters/μm²) |

## Combination Method

Connected localizations are combined using **maximum likelihood estimation (MLE) weighted mean**:
- Position: inverse-variance weighted average → `x_combined = Σ(x/σ²) / Σ(1/σ²)`
- Uncertainty: `σ_combined = √(1/Σ(1/σ²))` ≈ `σ_individual / √n`
- Photons: summed across connected localizations

## Utility Functions

### combinelocalizations
```julia
smld_combined = combinelocalizations(smld)
```
Combines emitters that share the same `track_id`. Use when you have pre-labeled data.
Combines emitters with the same `track_id`. Use when you have pre-labeled data.

### defineidealFC
```julia
smld_connected, smld_combined = defineidealFC(smld; maxframegap=5)
smld_connected, smld_combined = defineidealFC(smld; max_frame_gap=5)
```
For **simulated data** where `track_id` already contains ground-truth emitter IDs. Useful for validating frame-connection performance against known truth.
For simulated data where `track_id` contains ground-truth emitter IDs. Validates frame-connection performance against known truth.

## Algorithm Reference

> Schodt, D.J. and Lidke, K.A. "Spatiotemporal Clustering of Repeated Super-Resolution Localizations via Linear Assignment Problem." *Frontiers in Bioinformatics*, 2021. [DOI: 10.3389/fbinf.2021.724325](https://doi.org/10.3389/fbinf.2021.724325)

## Related Packages

## Citation
- **[SMLMAnalysis.jl](https://github.com/JuliaSMLM/SMLMAnalysis.jl)** - Complete SMLM workflow (detection + fitting + frame-connection + rendering)
- **[SMLMData.jl](https://github.com/JuliaSMLM/SMLMData.jl)** - Core data types for SMLM
- **[GaussMLE.jl](https://github.com/JuliaSMLM/GaussMLE.jl)** - GPU-accelerated Gaussian PSF fitting
- **[SMLMSim.jl](https://github.com/JuliaSMLM/SMLMSim.jl)** - SMLM data simulation

David J. Schodt and Keith A. Lidke, "Spatiotemporal Clustering of Repeated Super-Resolution Localizations via Linear Assignment Problem", Frontiers in Bioinformatics, 2021
## License

https://doi.org/10.3389/fbinf.2021.724325
MIT License - see [LICENSE](LICENSE) file for details.
Loading
Loading