From db88b904fc01785120c8a2777bbdfb70c58ff0f8 Mon Sep 17 00:00:00 2001 From: kalidke Date: Sun, 1 Feb 2026 17:37:23 -0700 Subject: [PATCH 01/11] Add tuple-pattern return type for frameconnect() BREAKING: frameconnect() now returns (combined, info) instead of NamedTuple - Add ConnectInfo struct with track assignments and algorithm metadata - Capture timing with elapsed_ns field - Include n_input, n_tracks, n_combined, n_preclusters counts - Rate parameters (k_on, k_off, k_bleach, p_miss) accessible via info - Update all tests, examples, and documentation - Bump version to 0.4.0 Co-Authored-By: Claude Opus 4.5 --- Project.toml | 2 +- README.md | 54 ++++++++---- api_overview.md | 50 +++++++---- examples/benchmark.jl | 14 ++-- examples/frameconnection_example.jl | 23 +++--- examples/fullscale_benchmark.jl | 8 +- examples/realistic_frameconnection.jl | 39 ++++----- src/SMLMFrameConnection.jl | 3 +- src/connectinfo.jl | 52 ++++++++++++ src/frameconnect.jl | 81 +++++++++++------- test/test_frameconnect.jl | 114 +++++++++++++++----------- test/test_types.jl | 27 ++++++ 12 files changed, 314 insertions(+), 153 deletions(-) create mode 100644 src/connectinfo.jl diff --git a/Project.toml b/Project.toml index 540df8e..3c889fc 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "SMLMFrameConnection" uuid = "1517b1a9-81e8-461f-b994-92eb29599690" authors = ["klidke@unm.edu"] -version = "0.3.0" +version = "0.4.0" [deps] Hungarian = "e91730f6-4275-51fb-a7a0-7064cfbd3b39" diff --git a/README.md b/README.md index 2885d8d..17961be 100644 --- a/README.md +++ b/README.md @@ -24,14 +24,15 @@ Pkg.add("SMLMFrameConnection") using SMLMData, SMLMFrameConnection # Run frame connection on your data -smld_connected, smld_preclustered, smld_combined, params = frameconnect(smld) +(combined, info) = frameconnect(smld) -# smld_combined is the main output - higher precision localizations +# combined is the main output - higher precision localizations +# info contains track assignments and algorithm metadata ``` ## Input Requirements -Input must be a `BasicSMLD{T, Emitter2DFit{T}}` from [SMLMData.jl](https://github.com/JuliaSMLM/SMLMData.jl). +Input must be a `BasicSMLD` from [SMLMData.jl](https://github.com/JuliaSMLM/SMLMData.jl) with emitters containing position uncertainties. **Required fields** (algorithm fails without these): - `x`, `y`: Position coordinates in microns @@ -52,41 +53,58 @@ using SMLMData, SMLMFrameConnection 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) +(combined, info) = frameconnect(smld) # Result: localizations connected based on spatial/temporal proximity # Combined uncertainty: σ_combined ≈ σ_individual / √n_connected +println("$(info.n_input) → $(info.n_combined) localizations") ``` ## Outputs Explained ```julia -smld_connected, smld_preclustered, smld_combined, params = frameconnect(smld) +(combined, info) = frameconnect(smld) ``` | 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 | +| `combined` | **Main output.** Combined high-precision localizations | Standard analysis | +| `info.connected` | Original localizations with `track_id` populated | When you need per-frame data with connection labels | +| `info.n_tracks` | Number of tracks formed | Summary statistics | +| `info.elapsed_ns` | Algorithm wall time in nanoseconds | Performance monitoring | + +### ConnectInfo Fields + +| Field | Type | Description | +|-------|------|-------------| +| `connected` | `BasicSMLD` | Input with `track_id` assigned (localizations uncombined) | +| `n_input` | `Int` | Number of input localizations | +| `n_tracks` | `Int` | Number of tracks formed | +| `n_combined` | `Int` | Number of output localizations | +| `k_on` | `Float64` | Estimated on rate (1/frame) | +| `k_off` | `Float64` | Estimated off rate (1/frame) | +| `k_bleach` | `Float64` | Estimated bleach rate (1/frame) | +| `p_miss` | `Float64` | Probability of missed detection | +| `initialdensity` | `Vector{Float64}` | Density estimate per cluster (emitters/μm²) | +| `elapsed_ns` | `UInt64` | Wall time in nanoseconds | +| `algorithm` | `Symbol` | Algorithm used (`:lap`) | +| `n_preclusters` | `Int` | Number of preclusters formed | ## Parameters @@ -104,9 +122,9 @@ frameconnect(smld; - `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) +## Estimated Photophysics -The algorithm estimates fluorophore photophysics from your data: +The algorithm estimates fluorophore photophysics from your data, accessible via `info`: | Field | Description | |-------|-------------| diff --git a/api_overview.md b/api_overview.md index aa34ece..f5651a9 100644 --- a/api_overview.md +++ b/api_overview.md @@ -6,12 +6,12 @@ Frame-connection for 2D SMLM data: combines repeated localizations of blinking f ### frameconnect ```julia -frameconnect(smld::BasicSMLD{T,Emitter2DFit{T}}; +(combined, info) = frameconnect(smld::BasicSMLD; nnearestclusters::Int=2, nsigmadev::Float64=5.0, maxframegap::Int=5, nmaxnn::Int=2 -) where T -> (smld_connected, smld_preclustered, smld_combined, params) +) ``` Main entry point. Connects repeated localizations and combines them. @@ -21,28 +21,48 @@ Main entry point. Connects repeated localizations and combines them. - `maxframegap`: Maximum frame gap for temporal adjacency in preclusters - `nmaxnn`: Maximum nearest-neighbors inspected for precluster membership -**Returns:** -- `smld_connected`: Input with `track_id` populated (localizations uncombined) -- `smld_preclustered`: Intermediate preclustering result (for debugging) -- `smld_combined`: **Main output** - combined high-precision localizations -- `params::ParamStruct`: Algorithm parameters (input + estimated photophysics) +**Returns tuple `(combined, info)`:** +- `combined::BasicSMLD`: **Main output** - combined high-precision localizations +- `info::ConnectInfo`: Track assignments and algorithm metadata ### combinelocalizations ```julia -combinelocalizations(smld::BasicSMLD{T,Emitter2DFit{T}}) where T -> BasicSMLD +combinelocalizations(smld::BasicSMLD) -> BasicSMLD ``` Combines emitters sharing the same `track_id` using MLE weighted mean. Use when `track_id` is already populated. ### defineidealFC ```julia -defineidealFC(smld::BasicSMLD{T,Emitter2DFit{T}}; - maxframegap::Int=5 -) where T -> (smld_connected, smld_combined) +defineidealFC(smld::BasicSMLD; maxframegap::Int=5) -> (smld_connected, smld_combined) ``` For simulated data where `track_id` indicates ground-truth emitter ID. Useful for validation/benchmarking. ## Types +### ConnectInfo{T} +```julia +struct ConnectInfo{T} + connected::BasicSMLD{T} # Input with track_id assigned (uncombined) + n_input::Int # Number of input localizations + n_tracks::Int # Number of tracks formed + n_combined::Int # Number of output localizations + k_on::Float64 # Rate: dark → visible (1/frame) + k_off::Float64 # Rate: visible → dark (1/frame) + k_bleach::Float64 # Rate: photobleaching (1/frame) + p_miss::Float64 # Probability of missed detection + initialdensity::Vector{Float64} # Emitter density per cluster (emitters/μm²) + elapsed_ns::UInt64 # Wall time in nanoseconds + algorithm::Symbol # Algorithm used (:lap) + n_preclusters::Int # Number of preclusters formed +end +``` + +**Access track assignments:** +```julia +(combined, info) = frameconnect(smld) +track_ids = [e.track_id for e in info.connected.emitters] +``` + ### ParamStruct ```julia mutable struct ParamStruct @@ -66,7 +86,7 @@ end ## Input Requirements -Input `BasicSMLD` must contain `Emitter2DFit` emitters. +Input `BasicSMLD` must contain emitters with position uncertainties. **Required fields:** - `x`, `y`: Position (microns) @@ -81,7 +101,7 @@ Input `BasicSMLD` must contain `Emitter2DFit` emitters. ## Output -Output `smld_combined` contains `Emitter2DFit` emitters with: +Output `combined` contains emitters with: - Combined position via MLE weighted mean: `x = Σ(x/σ²) / Σ(1/σ²)` - Reduced uncertainties: `σ = √(1/Σ(1/σ²))` - Summed photons @@ -89,8 +109,8 @@ Output `smld_combined` contains `Emitter2DFit` emitters with: ## Dependencies -- SMLMData.jl (v0.5+): BasicSMLD, Emitter2DFit types -- Hungarian.jl (v0.6-0.7): Linear assignment problem solver +- SMLMData.jl (v0.6+): BasicSMLD, Emitter types +- Hungarian.jl: Linear assignment problem solver - NearestNeighbors.jl: Spatial clustering - Optim.jl: Parameter estimation diff --git a/examples/benchmark.jl b/examples/benchmark.jl index ae61d70..b17675a 100644 --- a/examples/benchmark.jl +++ b/examples/benchmark.jl @@ -69,13 +69,14 @@ function benchmark_single(smld; warmup::Bool = false) GC.gc() # Clean up before measurement stats = @timed frameconnect(smld; nnearestclusters=2, nsigmadev=5.0, maxframegap=5, nmaxnn=2) + (combined, info) = stats.value return ( time = stats.time, bytes = stats.bytes, gctime = stats.gctime, n_input = length(smld.emitters), - n_output = length(stats.value[3].emitters) + n_output = length(combined.emitters) ) end @@ -162,7 +163,7 @@ function profile_detailed(; n_molecules::Int = 50, n_frames::Int = 100) println("-" ^ 70) GC.gc() - @time result = frameconnect( + @time (combined, info) = frameconnect( smld; nnearestclusters = 2, nsigmadev = 5.0, @@ -171,15 +172,16 @@ function profile_detailed(; n_molecules::Int = 50, n_frames::Int = 100) ) println("\nOutput:") - println(" Final tracks: $(length(unique(e.track_id for e in result.connected.emitters)))") - println(" Combined emitters: $(length(result.combined.emitters))") + println(" Final tracks: $(info.n_tracks)") + println(" Combined emitters: $(info.n_combined)") + println(" Algorithm time: $(info.elapsed_ns / 1e9)s") # Precision improvement input_σ = mean([e.σ_x for e in smld.emitters]) - combined_σ = mean([e.σ_x for e in result.combined.emitters]) + combined_σ = mean([e.σ_x for e in combined.emitters]) println("\nPrecision improvement: $(round(input_σ / combined_σ, digits=2))x") - return smld, result + return smld, (combined, info) end #= diff --git a/examples/frameconnection_example.jl b/examples/frameconnection_example.jl index 8c3e009..0621964 100644 --- a/examples/frameconnection_example.jl +++ b/examples/frameconnection_example.jl @@ -54,7 +54,7 @@ function generate_synthetic_smld(; e = Emitter2DFit{Float64}( obs_x, obs_y, photons, 10.0, # photons, bg - σ_loc, σ_loc, # σ_x, σ_y + σ_loc, σ_loc, 0.0, # σ_x, σ_y, σ_xy σ_photons, 3.0, # σ_photons, σ_bg frame, 1, mol_id, 0 # frame, dataset, track_id, id ) @@ -68,7 +68,7 @@ function generate_synthetic_smld(; # Reset track_id to 0 for input (algorithm will populate it) input_emitters = [Emitter2DFit{Float64}( - e.x, e.y, e.photons, e.bg, e.σ_x, e.σ_y, e.σ_photons, e.σ_bg, + e.x, e.y, e.photons, e.bg, e.σ_x, e.σ_y, e.σ_xy, e.σ_photons, e.σ_bg, e.frame, e.dataset, 0, e.id # track_id = 0 ) for e in emitters] @@ -95,7 +95,7 @@ println(" True molecules: $(length(unique(e.track_id for e in smld_truth.emitte # Run frame connection println("\nRunning frame connection...") -result = frameconnect( +(combined, info) = frameconnect( smld_input; nnearestclusters = 2, nsigmadev = 5.0, @@ -103,7 +103,8 @@ result = frameconnect( nmaxnn = 2 ) -println(" Combined localizations: $(length(result.combined.emitters))") +println(" Combined localizations: $(length(combined.emitters))") +println(" Time: $(info.elapsed_ns / 1e9) seconds") # Compare with ideal result (using ground truth track_id) println("\nComputing ideal frame connection (ground truth)...") @@ -112,22 +113,22 @@ println(" Ideal combined: $(length(smld_ideal_combined.emitters))") # Print estimated parameters println("\nEstimated photophysics parameters:") -println(" k_on (dark→visible rate): $(round(result.params.k_on, digits=4)) /frame") -println(" k_off (visible→dark rate): $(round(result.params.k_off, digits=4)) /frame") -println(" k_bleach (bleaching rate): $(round(result.params.k_bleach, digits=4)) /frame") -println(" p_miss (miss probability): $(round(result.params.p_miss, digits=4))") +println(" k_on (dark→visible rate): $(round(info.k_on, digits=4)) /frame") +println(" k_off (visible→dark rate): $(round(info.k_off, digits=4)) /frame") +println(" k_bleach (bleaching rate): $(round(info.k_bleach, digits=4)) /frame") +println(" p_miss (miss probability): $(round(info.p_miss, digits=4))") # Compute precision improvement input_σ = mean([e.σ_x for e in smld_input.emitters]) -combined_σ = mean([e.σ_x for e in result.combined.emitters]) +combined_σ = mean([e.σ_x for e in combined.emitters]) println("\nPrecision improvement:") println(" Mean input σ: $(round(input_σ * 1000, digits=1)) nm") println(" Mean combined σ: $(round(combined_σ * 1000, digits=1)) nm") println(" Improvement factor: $(round(input_σ / combined_σ, digits=2))x") # Summary statistics -n_unique_tracks = length(unique(e.track_id for e in smld_connected.emitters)) -avg_locs_per_track = length(smld_connected.emitters) / n_unique_tracks +n_unique_tracks = length(unique(e.track_id for e in info.connected.emitters)) +avg_locs_per_track = length(info.connected.emitters) / n_unique_tracks println("\nConnection statistics:") println(" Unique tracks identified: $n_unique_tracks") println(" Avg localizations per track: $(round(avg_locs_per_track, digits=1))") diff --git a/examples/fullscale_benchmark.jl b/examples/fullscale_benchmark.jl index 55974c7..ac4153d 100644 --- a/examples/fullscale_benchmark.jl +++ b/examples/fullscale_benchmark.jl @@ -90,14 +90,12 @@ function benchmark_single_dataset(smld::BasicSMLD) ) end - result = stats.value - n_output = length(result.combined.emitters) - n_tracks = length(unique(e.track_id for e in result.connected.emitters)) + (combined, info) = stats.value return ( n_input = n_input, - n_output = n_output, - n_tracks = n_tracks, + n_output = info.n_combined, + n_tracks = info.n_tracks, time = stats.time, bytes = stats.bytes, gctime = stats.gctime diff --git a/examples/realistic_frameconnection.jl b/examples/realistic_frameconnection.jl index 4f2fb23..e7d72f3 100644 --- a/examples/realistic_frameconnection.jl +++ b/examples/realistic_frameconnection.jl @@ -101,7 +101,7 @@ function main() # Clear track_id for input (algorithm should estimate it) input_emitters = [Emitter2DFit{Float64}( - e.x, e.y, e.photons, e.bg, e.σ_x, e.σ_y, e.σ_photons, e.σ_bg, + e.x, e.y, e.photons, e.bg, e.σ_x, e.σ_y, e.σ_xy, e.σ_photons, e.σ_bg, e.frame, e.dataset, 0, e.id # track_id = 0 ) for e in smld_noisy.emitters] @@ -126,8 +126,7 @@ function main() ) end - result = stats.value - fc_params = result.params + (combined, info) = stats.value @printf(" Time: %.2f seconds\n", stats.time) @printf(" Allocations: %.1f MB\n", stats.bytes / 1e6) @@ -135,15 +134,13 @@ function main() # ========================================================================= # 5. Results summary # ========================================================================= - n_tracks = length(unique(e.track_id for e in result.connected.emitters)) - n_combined = length(result.combined.emitters) - println("\n5. Frame Connection Results") println("-" ^ 70) - @printf(" Input localizations: %d\n", length(smld_input.emitters)) - @printf(" Connected tracks: %d\n", n_tracks) - @printf(" Combined emitters: %d\n", n_combined) - @printf(" Reduction ratio: %.1fx\n", length(smld_input.emitters) / n_combined) + @printf(" Input localizations: %d\n", info.n_input) + @printf(" Connected tracks: %d\n", info.n_tracks) + @printf(" Combined emitters: %d\n", info.n_combined) + @printf(" Preclusters formed: %d\n", info.n_preclusters) + @printf(" Reduction ratio: %.1fx\n", info.n_input / info.n_combined) # ========================================================================= # 6. Compare estimated vs true parameters @@ -159,12 +156,12 @@ function main() println(" Parameter True (Hz) Estimated (/frame) True (/frame)") println(" " * "-" ^ 60) @printf(" k_on %.2f %.4f %.6f\n", - 0.03, fc_params.k_on, true_k_on_pf) + 0.03, info.k_on, true_k_on_pf) @printf(" k_off %.2f %.4f %.4f\n", - 3.0, fc_params.k_off, true_k_off_pf) + 3.0, info.k_off, true_k_off_pf) @printf(" k_bleach N/A %.5f N/A (2-state model)\n", - fc_params.k_bleach) - @printf(" p_miss - %.4f -\n", fc_params.p_miss) + info.k_bleach) + @printf(" p_miss - %.4f -\n", info.p_miss) # ========================================================================= # 7. Precision improvement @@ -173,8 +170,8 @@ function main() println("-" ^ 70) input_σ = mean([e.σ_x for e in smld_input.emitters]) - combined_σ = mean([e.σ_x for e in result.combined.emitters]) - avg_locs_per_track = length(result.connected.emitters) / n_tracks + combined_σ = mean([e.σ_x for e in combined.emitters]) + avg_locs_per_track = info.n_input / info.n_tracks theoretical_improvement = sqrt(avg_locs_per_track) @printf(" Mean input σ: %.1f nm\n", input_σ * 1000) @@ -195,21 +192,21 @@ function main() @printf(" True emitters (SMLMSim): %d\n", n_true_emitters) @printf(" Ideal combined: %d\n", n_ideal) - @printf(" Algorithm combined: %d\n", n_combined) + @printf(" Algorithm combined: %d\n", info.n_combined) - if n_combined > n_ideal + if info.n_combined > n_ideal @printf(" Over-segmentation: %.1f%% (algorithm found more)\n", - 100.0 * (n_combined - n_ideal) / n_ideal) + 100.0 * (info.n_combined - n_ideal) / n_ideal) else @printf(" Under-segmentation: %.1f%% (algorithm merged too many)\n", - 100.0 * (n_ideal - n_combined) / n_ideal) + 100.0 * (n_ideal - info.n_combined) / n_ideal) end println("\n" * "=" ^ 70) println("Example complete!") println("=" ^ 70) - return smld_input, result, smld_noisy + return smld_input, (combined, info), smld_noisy end # Run diff --git a/src/SMLMFrameConnection.jl b/src/SMLMFrameConnection.jl index b2d34fd..1dc1af4 100644 --- a/src/SMLMFrameConnection.jl +++ b/src/SMLMFrameConnection.jl @@ -7,9 +7,10 @@ using Optim using StatsBase using Statistics -export frameconnect, defineidealFC, combinelocalizations, ParamStruct +export frameconnect, defineidealFC, combinelocalizations, ParamStruct, ConnectInfo include("structdefinitions.jl") +include("connectinfo.jl") include("precluster.jl") include("defineidealFC.jl") include("organizeclusters.jl") diff --git a/src/connectinfo.jl b/src/connectinfo.jl new file mode 100644 index 0000000..d12a24a --- /dev/null +++ b/src/connectinfo.jl @@ -0,0 +1,52 @@ +# ConnectInfo struct for tuple-pattern return + +""" + ConnectInfo{T} + +Secondary output from `frameconnect()` containing track assignments and algorithm metadata. + +# Fields +- `connected::BasicSMLD{T}`: Input SMLD with track_id assigned (localizations uncombined) +- `n_input::Int`: Number of input localizations +- `n_tracks::Int`: Number of tracks formed +- `n_combined::Int`: Number of output localizations +- `k_on::Float64`: Estimated on rate (1/frame) +- `k_off::Float64`: Estimated off rate (1/frame) +- `k_bleach::Float64`: Estimated bleach rate (1/frame) +- `p_miss::Float64`: Probability of missed detection +- `initialdensity::Vector{Float64}`: Initial density estimate per cluster (emitters/μm²) +- `elapsed_ns::UInt64`: Wall time in nanoseconds +- `algorithm::Symbol`: Algorithm used (`:lap`) +- `n_preclusters::Int`: Number of preclusters formed + +# Rate Parameter Interpretation + +The rate parameters describe the photophysics of blinking fluorophores: +- `k_on`: Rate at which dark emitters convert to visible state +- `k_off`: Rate at which visible emitters convert to reversible dark state +- `k_bleach`: Rate at which visible emitters are irreversibly photobleached +- Duty cycle = k_on / (k_on + k_off) +- For typical dSTORM: k_on << k_off (low duty cycle, mostly dark with brief blinks) + +# Example +```julia +(combined, info) = frameconnect(smld) +println("Connected \$(info.n_input) → \$(info.n_combined) localizations") +println("Formed \$(info.n_tracks) tracks in \$(info.elapsed_ns / 1e9)s") +# Access track assignments via info.connected +``` +""" +struct ConnectInfo{T} + connected::BasicSMLD{T} + n_input::Int + n_tracks::Int + n_combined::Int + k_on::Float64 + k_off::Float64 + k_bleach::Float64 + p_miss::Float64 + initialdensity::Vector{Float64} + elapsed_ns::UInt64 + algorithm::Symbol + n_preclusters::Int +end diff --git a/src/frameconnect.jl b/src/frameconnect.jl index b33cfea..669c4b4 100644 --- a/src/frameconnect.jl +++ b/src/frameconnect.jl @@ -1,48 +1,53 @@ using SMLMData """ - result = frameconnect(smld::BasicSMLD{T,E}; - nnearestclusters::Int=2, nsigmadev::Float64=5.0, - maxframegap::Int=5, nmaxnn::Int=2) where {T, E<:SMLMData.AbstractEmitter} + (combined, info) = frameconnect(smld::BasicSMLD{T,E}; kwargs...) Connect repeated localizations of the same emitter in `smld`. # Description Repeated localizations of the same emitter present in `smld` are connected and -combined into higher precision localizations of that emitter. This is done by +combined into higher precision localizations of that emitter. This is done by 1) forming pre-clusters of localizations, 2) estimating rate parameters from the pre-clusters, 3) solving a linear assignment problem for connecting localizations in each pre-cluster, and 4) combining the connected localizations using their MLE position estimate assuming Gaussian noise. -# Inputs -- `smld`: BasicSMLD containing the localizations that should be connected. - Must contain emitters with valid position uncertainties (σ_x, σ_y). -- `nnearestclusters`: Number of nearest preclusters used for local density - estimates. (default = 2)(see estimatedensities()) -- `nsigmadev`: Multiplier of localization errors that defines a pre-clustering - distance threshold. (default = 5)(see precluster())(microns) -- `maxframegap`: Maximum frame gap between temporally adjacent localizations in - a precluster. (default = 5)(see precluster())(frames) -- `nmaxnn`: Maximum number of nearest-neighbors inspected for precluster - membership. Ideally, this would be set to inf, but that's not - feasible for most data. (default = 2)(see precluster()) - -# Outputs -Returns a NamedTuple with fields: -- `combined`: Final frame-connection result (i.e., `smld` with localizations that - seem to be from the same blinking event combined into higher - precision localizations). -- `connected`: Input `smld` with track_id field updated to reflect connected - localizations (localizations remain uncombined). -- `params`: Structure of parameters used in the algorithm, with some copied - directly from the option kwargs to this function, and others - calculated internally (see SMLMFrameConnection.ParamStruct). +# Arguments +- `smld::BasicSMLD`: Localizations to connect. Must contain emitters with valid + position uncertainties (σ_x, σ_y). + +# Keyword Arguments +- `nnearestclusters::Int=2`: Number of nearest preclusters used for local density + estimates (see `estimatedensities`) +- `nsigmadev::Float64=5.0`: Multiplier of localization errors that defines a + pre-clustering distance threshold (see `precluster`) +- `maxframegap::Int=5`: Maximum frame gap between temporally adjacent localizations + in a precluster (see `precluster`) +- `nmaxnn::Int=2`: Maximum number of nearest-neighbors inspected for precluster + membership (see `precluster`) + +# Returns +A tuple `(combined, info)`: +- `combined::BasicSMLD`: Connected localizations combined into higher precision results +- `info::ConnectInfo`: Track assignments and algorithm metadata (see [`ConnectInfo`](@ref)) + +# Example +```julia +(combined, info) = frameconnect(smld) +println("Connected \$(info.n_input) → \$(info.n_combined) localizations") +println("Formed \$(info.n_tracks) tracks from \$(info.n_preclusters) preclusters") + +# Access track assignments for downstream analysis +track_ids = [e.track_id for e in info.connected.emitters] +``` """ function frameconnect(smld::BasicSMLD{T,E}; nnearestclusters::Int = 2, nsigmadev::Float64 = 5.0, maxframegap::Int = 5, nmaxnn::Int = 2) where {T, E<:SMLMData.AbstractEmitter} + t_start = time_ns() + # Prepare a ParamStruct to keep track of parameters used. params = ParamStruct() params.nnearestclusters = nnearestclusters @@ -53,6 +58,7 @@ function frameconnect(smld::BasicSMLD{T,E}; # Generate pre-clusters of localizations in `smld`. smld_preclustered = precluster(smld, params) clusterdata = organizeclusters(smld_preclustered) + n_preclusters = length(clusterdata) # Estimate rate parameters. params.k_on, params.k_off, params.k_bleach, params.p_miss = @@ -89,5 +95,24 @@ function frameconnect(smld::BasicSMLD{T,E}; # Combine the connected localizations into higher precision localizations. smld_combined = combinelocalizations(smld_connected) - return (combined=smld_combined, connected=smld_connected, params=params) + elapsed_ns = time_ns() - t_start + + # Build ConnectInfo + n_tracks = length(unique(connectID_final)) + info = ConnectInfo{T}( + smld_connected, + length(smld.emitters), + n_tracks, + length(smld_combined.emitters), + params.k_on, + params.k_off, + params.k_bleach, + params.p_miss, + params.initialdensity, + elapsed_ns, + :lap, + n_preclusters + ) + + return (smld_combined, info) end diff --git a/test/test_frameconnect.jl b/test/test_frameconnect.jl index 7ac4794..77fcdd8 100644 --- a/test/test_frameconnect.jl +++ b/test/test_frameconnect.jl @@ -1,19 +1,23 @@ @testset "frameconnect" begin - @testset "return types" begin + @testset "return types - tuple pattern" begin # Use multiple molecules for proper density estimation emitters = vcat( [make_emitter(Float64(i), Float64(i), j) for i in 1:5 for j in 1:3]... ) smld = make_test_smld(emitters; n_frames=3) - result = frameconnect(smld) - - @test result isa NamedTuple - @test haskey(result, :combined) - @test haskey(result, :connected) - @test haskey(result, :params) - @test result.combined isa BasicSMLD - @test result.connected isa BasicSMLD - @test result.params isa ParamStruct + (combined, info) = frameconnect(smld) + + @test combined isa BasicSMLD + @test info isa ConnectInfo + + # ConnectInfo fields + @test info.connected isa BasicSMLD + @test info.n_input == length(smld.emitters) + @test info.n_tracks > 0 + @test info.n_combined == length(combined.emitters) + @test info.elapsed_ns > 0 + @test info.algorithm == :lap + @test info.n_preclusters > 0 end @testset "single molecule connection" begin @@ -25,11 +29,13 @@ ) smld = make_test_smld(emitters; n_frames=5) - result = frameconnect(smld; maxframegap=5, nnearestclusters=1) + (combined, info) = frameconnect(smld; maxframegap=5, nnearestclusters=1) # Target molecule should be combined (may have 5 background singles) # At minimum, we should have fewer emitters than input - @test length(result.combined.emitters) < length(smld.emitters) + @test length(combined.emitters) < length(smld.emitters) + @test info.n_input == length(smld.emitters) + @test info.n_combined == length(combined.emitters) end @testset "multiple separated molecules" begin @@ -42,11 +48,11 @@ ) smld = make_test_smld(emitters; n_frames=3) - result = frameconnect(smld; maxframegap=5) + (combined, info) = frameconnect(smld; maxframegap=5) # Should combine into ~4 molecules (fewer than 12 input emitters) - @test length(result.combined.emitters) <= 8 - @test length(result.combined.emitters) < length(smld.emitters) + @test length(combined.emitters) <= 8 + @test length(combined.emitters) < length(smld.emitters) end @testset "track_id population" begin @@ -57,15 +63,15 @@ ) smld = make_test_smld(emitters; n_frames=3) - result = frameconnect(smld) + (combined, info) = frameconnect(smld) - # All emitters should have non-zero track_id - for e in result.connected.emitters + # All emitters in info.connected should have non-zero track_id + for e in info.connected.emitters @test e.track_id > 0 end end - @testset "parameter estimation" begin + @testset "rate parameter estimation" begin emitters = vcat( make_blinking_molecule(5.0, 5.0, [1, 2, 3]), make_blinking_molecule(10.0, 10.0, [1, 2, 3]), @@ -74,14 +80,14 @@ ) smld = make_test_smld(emitters; n_frames=3) - result = frameconnect(smld) + (combined, info) = frameconnect(smld) # Parameters should be estimated (non-default values) - @test result.params.k_on >= 0 - @test result.params.k_off >= 0 - @test result.params.k_bleach >= 0 - @test 0 <= result.params.p_miss <= 1 - @test !isempty(result.params.initialdensity) + @test info.k_on >= 0 + @test info.k_off >= 0 + @test info.k_bleach >= 0 + @test 0 <= info.p_miss <= 1 + @test !isempty(info.initialdensity) end @testset "respects maxframegap" begin @@ -95,10 +101,10 @@ smld = make_test_smld(emitters; n_frames=12) # With maxframegap=5, the two groups at (5,5) should NOT connect - result = frameconnect(smld; maxframegap=5) + (combined, info) = frameconnect(smld; maxframegap=5) # Should have at least 3 combined localizations (2 for split molecule + 2 background) - @test length(result.combined.emitters) >= 3 + @test length(combined.emitters) >= 3 end @testset "single localization" begin @@ -106,20 +112,22 @@ emitters = [make_emitter(5.0, 5.0, 1)] smld = make_test_smld(emitters; n_frames=1) - result = frameconnect(smld) + (combined, info) = frameconnect(smld) - @test length(result.combined.emitters) == 1 - @test result.combined.emitters[1].x ≈ 5.0 atol=1e-10 + @test length(combined.emitters) == 1 + @test combined.emitters[1].x ≈ 5.0 atol=1e-10 + @test info.n_input == 1 + @test info.n_combined == 1 end @testset "preserves camera and metadata" begin smld = make_single_molecule_smld() - result = frameconnect(smld) + (combined, info) = frameconnect(smld) - @test result.connected.camera == smld.camera - @test result.combined.camera == smld.camera - @test result.connected.n_datasets == smld.n_datasets + @test info.connected.camera == smld.camera + @test combined.camera == smld.camera + @test info.connected.n_datasets == smld.n_datasets end @testset "precision improvement" begin @@ -136,10 +144,10 @@ emitters = vcat(target_emitters, background) smld = make_test_smld(emitters; n_frames=n_locs) - result = frameconnect(smld; maxframegap=n_locs, nnearestclusters=1) + (combined, info) = frameconnect(smld; maxframegap=n_locs, nnearestclusters=1) # Find the combined target molecule (around position 5,5) - target_combined = filter(e -> e.x < 10.0 && e.y < 10.0, result.combined.emitters) + target_combined = filter(e -> e.x < 10.0 && e.y < 10.0, combined.emitters) @test length(target_combined) == 1 σ_output = target_combined[1].σ_x @@ -149,19 +157,31 @@ @test σ_output ≈ expected_σ rtol=0.1 end - @testset "custom parameters" begin + @testset "timing capture" begin smld = make_single_molecule_smld() - result = frameconnect(smld; - nnearestclusters=3, - nsigmadev=4.0, - maxframegap=10, - nmaxnn=3 - ) + (combined, info) = frameconnect(smld) + + # elapsed_ns should be positive and reasonable (< 60 seconds) + @test info.elapsed_ns > 0 + @test info.elapsed_ns < 60_000_000_000 # < 60s + end - @test result.params.nnearestclusters == 3 - @test result.params.nsigmadev == 4.0 - @test result.params.maxframegap == 10 - @test result.params.nmaxnn == 3 + @testset "ConnectInfo type parameter" begin + # Test with Float64 + smld64 = make_single_molecule_smld() + (_, info64) = frameconnect(smld64) + @test info64 isa ConnectInfo{Float64} + + # Test with Float32 + emitters32 = [SMLMData.Emitter2DFit{Float32}( + 5.0f0, 5.0f0, 1000.0f0, 10.0f0, + 0.02f0, 0.02f0, 0.0f0, 10.0f0, 1.0f0, + 1, 1, 0, 1 + )] + camera32 = SMLMData.IdealCamera(1:64, 1:64, 0.1f0) + smld32 = BasicSMLD(emitters32, camera32, 1, 1, Dict{String,Any}()) + (_, info32) = frameconnect(smld32) + @test info32 isa ConnectInfo{Float32} end end diff --git a/test/test_types.jl b/test/test_types.jl index 178fded..706a75f 100644 --- a/test/test_types.jl +++ b/test/test_types.jl @@ -1,4 +1,31 @@ @testset "Types" begin + @testset "ConnectInfo" begin + # Create minimal test data + emitters = [SMLMData.Emitter2DFit{Float64}( + 5.0, 5.0, 1000.0, 10.0, 0.02, 0.02, 0.0, 10.0, 1.0, 1, 1, 1, 1 + )] + camera = SMLMData.IdealCamera(1:64, 1:64, 0.1) + smld = BasicSMLD(emitters, camera, 1, 1, Dict{String,Any}()) + + info = ConnectInfo{Float64}( + smld, 10, 5, 5, 0.1, 0.5, 0.01, 0.05, [1.0, 2.0], UInt64(1_000_000), :lap, 3 + ) + + @test info isa ConnectInfo{Float64} + @test info.connected === smld + @test info.n_input == 10 + @test info.n_tracks == 5 + @test info.n_combined == 5 + @test info.k_on == 0.1 + @test info.k_off == 0.5 + @test info.k_bleach == 0.01 + @test info.p_miss == 0.05 + @test info.initialdensity == [1.0, 2.0] + @test info.elapsed_ns == UInt64(1_000_000) + @test info.algorithm == :lap + @test info.n_preclusters == 3 + end + @testset "ParamStruct" begin @testset "default constructor" begin params = ParamStruct() From d28a381c11de7c0781c58704174957f9424f44a4 Mon Sep 17 00:00:00 2001 From: kalidke Date: Mon, 2 Feb 2026 15:47:19 -0700 Subject: [PATCH 02/11] Change elapsed_ns to elapsed_s (Float64 seconds) Co-Authored-By: Claude Opus 4.5 --- README.md | 4 ++-- api_overview.md | 2 +- examples/benchmark.jl | 2 +- examples/frameconnection_example.jl | 2 +- src/connectinfo.jl | 6 +++--- src/frameconnect.jl | 6 +++--- test/test_frameconnect.jl | 8 ++++---- test/test_types.jl | 4 ++-- 8 files changed, 17 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 17961be..ebf9d79 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ println("$(info.n_input) → $(info.n_combined) localizations") | `combined` | **Main output.** Combined high-precision localizations | Standard analysis | | `info.connected` | Original localizations with `track_id` populated | When you need per-frame data with connection labels | | `info.n_tracks` | Number of tracks formed | Summary statistics | -| `info.elapsed_ns` | Algorithm wall time in nanoseconds | Performance monitoring | +| `info.elapsed_s` | Algorithm wall time in seconds | Performance monitoring | ### ConnectInfo Fields @@ -102,7 +102,7 @@ println("$(info.n_input) → $(info.n_combined) localizations") | `k_bleach` | `Float64` | Estimated bleach rate (1/frame) | | `p_miss` | `Float64` | Probability of missed detection | | `initialdensity` | `Vector{Float64}` | Density estimate per cluster (emitters/μm²) | -| `elapsed_ns` | `UInt64` | Wall time in nanoseconds | +| `elapsed_s` | `Float64` | Wall time in seconds | | `algorithm` | `Symbol` | Algorithm used (`:lap`) | | `n_preclusters` | `Int` | Number of preclusters formed | diff --git a/api_overview.md b/api_overview.md index f5651a9..c350b5e 100644 --- a/api_overview.md +++ b/api_overview.md @@ -51,7 +51,7 @@ struct ConnectInfo{T} k_bleach::Float64 # Rate: photobleaching (1/frame) p_miss::Float64 # Probability of missed detection initialdensity::Vector{Float64} # Emitter density per cluster (emitters/μm²) - elapsed_ns::UInt64 # Wall time in nanoseconds + elapsed_s::Float64 # Wall time in seconds algorithm::Symbol # Algorithm used (:lap) n_preclusters::Int # Number of preclusters formed end diff --git a/examples/benchmark.jl b/examples/benchmark.jl index b17675a..e097b70 100644 --- a/examples/benchmark.jl +++ b/examples/benchmark.jl @@ -174,7 +174,7 @@ function profile_detailed(; n_molecules::Int = 50, n_frames::Int = 100) println("\nOutput:") println(" Final tracks: $(info.n_tracks)") println(" Combined emitters: $(info.n_combined)") - println(" Algorithm time: $(info.elapsed_ns / 1e9)s") + println(" Algorithm time: $(info.elapsed_s)s") # Precision improvement input_σ = mean([e.σ_x for e in smld.emitters]) diff --git a/examples/frameconnection_example.jl b/examples/frameconnection_example.jl index 0621964..b6c52bc 100644 --- a/examples/frameconnection_example.jl +++ b/examples/frameconnection_example.jl @@ -104,7 +104,7 @@ println("\nRunning frame connection...") ) println(" Combined localizations: $(length(combined.emitters))") -println(" Time: $(info.elapsed_ns / 1e9) seconds") +println(" Time: $(info.elapsed_s) seconds") # Compare with ideal result (using ground truth track_id) println("\nComputing ideal frame connection (ground truth)...") diff --git a/src/connectinfo.jl b/src/connectinfo.jl index d12a24a..ec26b78 100644 --- a/src/connectinfo.jl +++ b/src/connectinfo.jl @@ -15,7 +15,7 @@ Secondary output from `frameconnect()` containing track assignments and algorith - `k_bleach::Float64`: Estimated bleach rate (1/frame) - `p_miss::Float64`: Probability of missed detection - `initialdensity::Vector{Float64}`: Initial density estimate per cluster (emitters/μm²) -- `elapsed_ns::UInt64`: Wall time in nanoseconds +- `elapsed_s::Float64`: Wall time in seconds - `algorithm::Symbol`: Algorithm used (`:lap`) - `n_preclusters::Int`: Number of preclusters formed @@ -32,7 +32,7 @@ The rate parameters describe the photophysics of blinking fluorophores: ```julia (combined, info) = frameconnect(smld) println("Connected \$(info.n_input) → \$(info.n_combined) localizations") -println("Formed \$(info.n_tracks) tracks in \$(info.elapsed_ns / 1e9)s") +println("Formed \$(info.n_tracks) tracks in \$(info.elapsed_s)s") # Access track assignments via info.connected ``` """ @@ -46,7 +46,7 @@ struct ConnectInfo{T} k_bleach::Float64 p_miss::Float64 initialdensity::Vector{Float64} - elapsed_ns::UInt64 + elapsed_s::Float64 algorithm::Symbol n_preclusters::Int end diff --git a/src/frameconnect.jl b/src/frameconnect.jl index 669c4b4..835a14f 100644 --- a/src/frameconnect.jl +++ b/src/frameconnect.jl @@ -46,7 +46,7 @@ function frameconnect(smld::BasicSMLD{T,E}; nnearestclusters::Int = 2, nsigmadev::Float64 = 5.0, maxframegap::Int = 5, nmaxnn::Int = 2) where {T, E<:SMLMData.AbstractEmitter} - t_start = time_ns() + t_start = time() # Prepare a ParamStruct to keep track of parameters used. params = ParamStruct() @@ -95,7 +95,7 @@ function frameconnect(smld::BasicSMLD{T,E}; # Combine the connected localizations into higher precision localizations. smld_combined = combinelocalizations(smld_connected) - elapsed_ns = time_ns() - t_start + elapsed_s = time() - t_start # Build ConnectInfo n_tracks = length(unique(connectID_final)) @@ -109,7 +109,7 @@ function frameconnect(smld::BasicSMLD{T,E}; params.k_bleach, params.p_miss, params.initialdensity, - elapsed_ns, + elapsed_s, :lap, n_preclusters ) diff --git a/test/test_frameconnect.jl b/test/test_frameconnect.jl index 77fcdd8..88703f3 100644 --- a/test/test_frameconnect.jl +++ b/test/test_frameconnect.jl @@ -15,7 +15,7 @@ @test info.n_input == length(smld.emitters) @test info.n_tracks > 0 @test info.n_combined == length(combined.emitters) - @test info.elapsed_ns > 0 + @test info.elapsed_s > 0 @test info.algorithm == :lap @test info.n_preclusters > 0 end @@ -162,9 +162,9 @@ (combined, info) = frameconnect(smld) - # elapsed_ns should be positive and reasonable (< 60 seconds) - @test info.elapsed_ns > 0 - @test info.elapsed_ns < 60_000_000_000 # < 60s + # elapsed_s should be positive and reasonable (< 60 seconds) + @test info.elapsed_s > 0 + @test info.elapsed_s < 60.0 end @testset "ConnectInfo type parameter" begin diff --git a/test/test_types.jl b/test/test_types.jl index 706a75f..0b41d64 100644 --- a/test/test_types.jl +++ b/test/test_types.jl @@ -8,7 +8,7 @@ smld = BasicSMLD(emitters, camera, 1, 1, Dict{String,Any}()) info = ConnectInfo{Float64}( - smld, 10, 5, 5, 0.1, 0.5, 0.01, 0.05, [1.0, 2.0], UInt64(1_000_000), :lap, 3 + smld, 10, 5, 5, 0.1, 0.5, 0.01, 0.05, [1.0, 2.0], 1.5, :lap, 3 ) @test info isa ConnectInfo{Float64} @@ -21,7 +21,7 @@ @test info.k_bleach == 0.01 @test info.p_miss == 0.05 @test info.initialdensity == [1.0, 2.0] - @test info.elapsed_ns == UInt64(1_000_000) + @test info.elapsed_s == 1.5 @test info.algorithm == :lap @test info.n_preclusters == 3 end From bb735631f40aceacfe060b06a17c09cc2a370b37 Mon Sep 17 00:00:00 2001 From: kalidke Date: Wed, 4 Feb 2026 07:47:33 -0700 Subject: [PATCH 03/11] Add ConnectConfig struct for frameconnect parameters - Add ConnectConfig with @kwdef for algorithm parameters - Support both calling conventions: - frameconnect(smld, config::ConnectConfig) - frameconnect(smld; kwargs...) forwards to config form - Export ConnectConfig from module - All 115 tests pass Co-Authored-By: Claude Opus 4.5 --- src/SMLMFrameConnection.jl | 3 ++- src/connectlocalizations.jl | 4 ++-- src/frameconnect.jl | 30 +++++++++++++++++++--------- src/structdefinitions.jl | 40 +++++++++++++++++++++++++++++++++++-- 4 files changed, 63 insertions(+), 14 deletions(-) diff --git a/src/SMLMFrameConnection.jl b/src/SMLMFrameConnection.jl index 1dc1af4..d07b786 100644 --- a/src/SMLMFrameConnection.jl +++ b/src/SMLMFrameConnection.jl @@ -7,7 +7,8 @@ using Optim using StatsBase using Statistics -export frameconnect, defineidealFC, combinelocalizations, ParamStruct, ConnectInfo +export frameconnect, defineidealFC, combinelocalizations +export ConnectConfig, ConnectInfo, ParamStruct include("structdefinitions.jl") include("connectinfo.jl") diff --git a/src/connectlocalizations.jl b/src/connectlocalizations.jl index df9bc8b..62f957c 100644 --- a/src/connectlocalizations.jl +++ b/src/connectlocalizations.jl @@ -1,8 +1,8 @@ using StatsBase """ - connectlocalizations!(connectID::Vector{Int64}, - clusterdata::Vector{Matrix{Float32}}, + connectlocalizations!(connectID::Vector{Int64}, + clusterdata::Vector{Matrix{Float32}}, params::ParamStruct, nframes::Int64) Connect localizations in `clusterdata` by solving a linear assignment problem. diff --git a/src/frameconnect.jl b/src/frameconnect.jl index 835a14f..48cfa6e 100644 --- a/src/frameconnect.jl +++ b/src/frameconnect.jl @@ -1,7 +1,8 @@ using SMLMData """ - (combined, info) = frameconnect(smld::BasicSMLD{T,E}; kwargs...) + (combined, info) = frameconnect(smld::BasicSMLD, config::ConnectConfig) + (combined, info) = frameconnect(smld::BasicSMLD; kwargs...) Connect repeated localizations of the same emitter in `smld`. @@ -16,8 +17,9 @@ using their MLE position estimate assuming Gaussian noise. # Arguments - `smld::BasicSMLD`: Localizations to connect. Must contain emitters with valid position uncertainties (σ_x, σ_y). +- `config::ConnectConfig`: Configuration parameters (optional, can use kwargs instead) -# Keyword Arguments +# Keyword Arguments (equivalent to ConnectConfig fields) - `nnearestclusters::Int=2`: Number of nearest preclusters used for local density estimates (see `estimatedensities`) - `nsigmadev::Float64=5.0`: Multiplier of localization errors that defines a @@ -34,7 +36,14 @@ A tuple `(combined, info)`: # Example ```julia +# Using kwargs (most common) (combined, info) = frameconnect(smld) +(combined, info) = frameconnect(smld; maxframegap=10) + +# Using config struct +config = ConnectConfig(maxframegap=10, nsigmadev=3.0) +(combined, info) = frameconnect(smld, config) + println("Connected \$(info.n_input) → \$(info.n_combined) localizations") println("Formed \$(info.n_tracks) tracks from \$(info.n_preclusters) preclusters") @@ -42,18 +51,21 @@ println("Formed \$(info.n_tracks) tracks from \$(info.n_preclusters) preclusters track_ids = [e.track_id for e in info.connected.emitters] ``` """ -function frameconnect(smld::BasicSMLD{T,E}; - nnearestclusters::Int = 2, nsigmadev::Float64 = 5.0, - maxframegap::Int = 5, nmaxnn::Int = 2) where {T, E<:SMLMData.AbstractEmitter} +function frameconnect(smld::BasicSMLD{T,E}; kwargs...) where {T, E<:SMLMData.AbstractEmitter} + # kwargs form forwards to config form + config = ConnectConfig(; kwargs...) + return frameconnect(smld, config) +end +function frameconnect(smld::BasicSMLD{T,E}, config::ConnectConfig) where {T, E<:SMLMData.AbstractEmitter} t_start = time() # Prepare a ParamStruct to keep track of parameters used. params = ParamStruct() - params.nnearestclusters = nnearestclusters - params.nsigmadev = nsigmadev - params.maxframegap = maxframegap - params.nmaxnn = nmaxnn + params.nnearestclusters = config.nnearestclusters + params.nsigmadev = config.nsigmadev + params.maxframegap = config.maxframegap + params.nmaxnn = config.nmaxnn # Generate pre-clusters of localizations in `smld`. smld_preclustered = precluster(smld, params) diff --git a/src/structdefinitions.jl b/src/structdefinitions.jl index a652b3d..c340c30 100644 --- a/src/structdefinitions.jl +++ b/src/structdefinitions.jl @@ -1,8 +1,44 @@ # This file defines some struct types used in the FrameConnection package. """ - ParamStruct(initialdensity::Vector{Float64}, - k_on::Float64, k_off::Float64, k_bleach::Float64, p_miss::Float64, + ConnectConfig + +Configuration parameters for frame connection algorithm. + +# Fields +- `nnearestclusters::Int=2`: Number of nearest preclusters used for local density + estimates (see `estimatedensities`) +- `nsigmadev::Float64=5.0`: Multiplier of localization errors that defines a + pre-clustering distance threshold (see `precluster`) +- `maxframegap::Int=5`: Maximum frame gap between temporally adjacent localizations + in a precluster (see `precluster`) +- `nmaxnn::Int=2`: Maximum number of nearest-neighbors inspected for precluster + membership (see `precluster`) + +# Example +```julia +# Using default config +config = ConnectConfig() +(combined, info) = frameconnect(smld, config) + +# Custom config +config = ConnectConfig(maxframegap=10, nsigmadev=3.0) +(combined, info) = frameconnect(smld, config) + +# Kwargs form (equivalent to Config form) +(combined, info) = frameconnect(smld; maxframegap=10, nsigmadev=3.0) +``` +""" +Base.@kwdef struct ConnectConfig + nnearestclusters::Int = 2 + nsigmadev::Float64 = 5.0 + maxframegap::Int = 5 + nmaxnn::Int = 2 +end + +""" + ParamStruct(initialdensity::Vector{Float64}, + k_on::Float64, k_off::Float64, k_bleach::Float64, p_miss::Float64, nsigmadev::Float64, maxframegap::Int, nnearestclusters::Int) Structure of parameters needed for frame-connection. From 64f91c2e2d8a013873d6c245e6b8afc19e507662 Mon Sep 17 00:00:00 2001 From: kalidke Date: Wed, 4 Feb 2026 07:52:24 -0700 Subject: [PATCH 04/11] Update docs with ConnectConfig struct and calling conventions - README: Add Configuration section with kwargs and config struct examples - api_overview: Add ConnectConfig type, update frameconnect signatures Co-Authored-By: Claude Opus 4.5 --- README.md | 22 ++++++++++++++++++++-- api_overview.md | 38 ++++++++++++++++++++++++++------------ 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index ebf9d79..40e4258 100644 --- a/README.md +++ b/README.md @@ -106,10 +106,13 @@ println("$(info.n_input) → $(info.n_combined) localizations") | `algorithm` | `Symbol` | Algorithm used (`:lap`) | | `n_preclusters` | `Int` | Number of preclusters formed | -## Parameters +## Configuration +Two equivalent ways to configure `frameconnect`: + +### Keyword Arguments (most common) ```julia -frameconnect(smld; +(combined, info) = 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 @@ -117,6 +120,21 @@ frameconnect(smld; ) ``` +### Config Struct (for reusable/shareable settings) +```julia +config = ConnectConfig(maxframegap=10, nsigmadev=3.0) +(combined, info) = frameconnect(smld, config) +``` + +### ConnectConfig Fields + +| Field | Default | Description | +|-------|---------|-------------| +| `nnearestclusters` | 2 | Nearest preclusters for local density estimation | +| `nsigmadev` | 5.0 | Sigma multiplier for preclustering distance threshold | +| `maxframegap` | 5 | Maximum frame gap for temporal adjacency | +| `nmaxnn` | 2 | Maximum nearest-neighbors for precluster membership | + **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. diff --git a/api_overview.md b/api_overview.md index c350b5e..5c7f592 100644 --- a/api_overview.md +++ b/api_overview.md @@ -6,21 +6,14 @@ Frame-connection for 2D SMLM data: combines repeated localizations of blinking f ### frameconnect ```julia -(combined, info) = frameconnect(smld::BasicSMLD; - nnearestclusters::Int=2, - nsigmadev::Float64=5.0, - maxframegap::Int=5, - nmaxnn::Int=2 -) +# Kwargs form (most common) +(combined, info) = frameconnect(smld::BasicSMLD; kwargs...) + +# Config form (reusable settings) +(combined, info) = frameconnect(smld::BasicSMLD, config::ConnectConfig) ``` Main entry point. Connects repeated localizations and combines them. -**Parameters:** -- `nnearestclusters`: Nearest preclusters for local density estimation -- `nsigmadev`: Sigma multiplier defining preclustering distance threshold (higher = larger connection radius) -- `maxframegap`: Maximum frame gap for temporal adjacency in preclusters -- `nmaxnn`: Maximum nearest-neighbors inspected for precluster membership - **Returns tuple `(combined, info)`:** - `combined::BasicSMLD`: **Main output** - combined high-precision localizations - `info::ConnectInfo`: Track assignments and algorithm metadata @@ -39,6 +32,27 @@ For simulated data where `track_id` indicates ground-truth emitter ID. Useful fo ## Types +### ConnectConfig +```julia +@kwdef struct ConnectConfig + nnearestclusters::Int = 2 # Nearest preclusters for local density estimation + nsigmadev::Float64 = 5.0 # Sigma multiplier for preclustering distance threshold + maxframegap::Int = 5 # Maximum frame gap for temporal adjacency + nmaxnn::Int = 2 # Maximum nearest-neighbors for precluster membership +end +``` +Configuration parameters for frame connection. Use with `frameconnect(smld, config)` or pass fields as kwargs to `frameconnect(smld; kwargs...)`. + +**Example:** +```julia +# Create config with custom settings +config = ConnectConfig(maxframegap=10, nsigmadev=3.0) +(combined, info) = frameconnect(smld, config) + +# Equivalent kwargs form +(combined, info) = frameconnect(smld; maxframegap=10, nsigmadev=3.0) +``` + ### ConnectInfo{T} ```julia struct ConnectInfo{T} From 25ad126da09cebde8ba7812a2f596df80eacef22 Mon Sep 17 00:00:00 2001 From: kalidke Date: Wed, 4 Feb 2026 07:54:43 -0700 Subject: [PATCH 05/11] Bump version to 0.5.0 Co-Authored-By: Claude Opus 4.5 --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 3c889fc..6cb6df6 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "SMLMFrameConnection" uuid = "1517b1a9-81e8-461f-b994-92eb29599690" authors = ["klidke@unm.edu"] -version = "0.4.0" +version = "0.5.0" [deps] Hungarian = "e91730f6-4275-51fb-a7a0-7064cfbd3b39" From 5eaf8da03804f927d624cb6858cb48d186dadd92 Mon Sep 17 00:00:00 2001 From: kalidke Date: Fri, 6 Feb 2026 09:43:27 -0700 Subject: [PATCH 06/11] Add AbstractSMLMConfig/AbstractSMLMInfo inheritance ConnectConfig <: AbstractSMLMConfig, ConnectInfo <: AbstractSMLMInfo from SMLMData v0.6.0 (feature/tuple-pattern c366f68). Co-Authored-By: Claude Opus 4.6 --- src/SMLMFrameConnection.jl | 1 + src/connectinfo.jl | 2 +- src/structdefinitions.jl | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/SMLMFrameConnection.jl b/src/SMLMFrameConnection.jl index d07b786..29047bc 100644 --- a/src/SMLMFrameConnection.jl +++ b/src/SMLMFrameConnection.jl @@ -1,6 +1,7 @@ module SMLMFrameConnection using SMLMData +import SMLMData: AbstractSMLMConfig, AbstractSMLMInfo using Hungarian using NearestNeighbors using Optim diff --git a/src/connectinfo.jl b/src/connectinfo.jl index ec26b78..340205a 100644 --- a/src/connectinfo.jl +++ b/src/connectinfo.jl @@ -36,7 +36,7 @@ println("Formed \$(info.n_tracks) tracks in \$(info.elapsed_s)s") # Access track assignments via info.connected ``` """ -struct ConnectInfo{T} +struct ConnectInfo{T} <: AbstractSMLMInfo connected::BasicSMLD{T} n_input::Int n_tracks::Int diff --git a/src/structdefinitions.jl b/src/structdefinitions.jl index c340c30..4520d68 100644 --- a/src/structdefinitions.jl +++ b/src/structdefinitions.jl @@ -29,7 +29,7 @@ config = ConnectConfig(maxframegap=10, nsigmadev=3.0) (combined, info) = frameconnect(smld; maxframegap=10, nsigmadev=3.0) ``` """ -Base.@kwdef struct ConnectConfig +Base.@kwdef struct ConnectConfig <: AbstractSMLMConfig nnearestclusters::Int = 2 nsigmadev::Float64 = 5.0 maxframegap::Int = 5 From d3858da8f40258478c657544da8d72f790c8ebdd Mon Sep 17 00:00:00 2001 From: kalidke Date: Fri, 6 Feb 2026 11:18:30 -0700 Subject: [PATCH 07/11] Rename ConnectConfig -> FrameConnectConfig, ConnectInfo -> FrameConnectInfo Namespace-qualified type names across struct definitions, exports, tests, docs, README, and api_overview.md. Zero old names remain. Co-Authored-By: Claude Opus 4.6 --- README.md | 6 +++--- api_overview.md | 14 +++++++------- src/SMLMFrameConnection.jl | 2 +- src/connectinfo.jl | 6 +++--- src/frameconnect.jl | 18 +++++++++--------- src/structdefinitions.jl | 8 ++++---- test/test_frameconnect.jl | 10 +++++----- test/test_types.jl | 6 +++--- 8 files changed, 35 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 40e4258..aeaf1ec 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ println("$(info.n_input) → $(info.n_combined) localizations") | `info.n_tracks` | Number of tracks formed | Summary statistics | | `info.elapsed_s` | Algorithm wall time in seconds | Performance monitoring | -### ConnectInfo Fields +### FrameConnectInfo Fields | Field | Type | Description | |-------|------|-------------| @@ -122,11 +122,11 @@ Two equivalent ways to configure `frameconnect`: ### Config Struct (for reusable/shareable settings) ```julia -config = ConnectConfig(maxframegap=10, nsigmadev=3.0) +config = FrameConnectConfig(maxframegap=10, nsigmadev=3.0) (combined, info) = frameconnect(smld, config) ``` -### ConnectConfig Fields +### FrameConnectConfig Fields | Field | Default | Description | |-------|---------|-------------| diff --git a/api_overview.md b/api_overview.md index 5c7f592..7860bdb 100644 --- a/api_overview.md +++ b/api_overview.md @@ -10,13 +10,13 @@ Frame-connection for 2D SMLM data: combines repeated localizations of blinking f (combined, info) = frameconnect(smld::BasicSMLD; kwargs...) # Config form (reusable settings) -(combined, info) = frameconnect(smld::BasicSMLD, config::ConnectConfig) +(combined, info) = frameconnect(smld::BasicSMLD, config::FrameConnectConfig) ``` Main entry point. Connects repeated localizations and combines them. **Returns tuple `(combined, info)`:** - `combined::BasicSMLD`: **Main output** - combined high-precision localizations -- `info::ConnectInfo`: Track assignments and algorithm metadata +- `info::FrameConnectInfo`: Track assignments and algorithm metadata ### combinelocalizations ```julia @@ -32,9 +32,9 @@ For simulated data where `track_id` indicates ground-truth emitter ID. Useful fo ## Types -### ConnectConfig +### FrameConnectConfig ```julia -@kwdef struct ConnectConfig +@kwdef struct FrameConnectConfig nnearestclusters::Int = 2 # Nearest preclusters for local density estimation nsigmadev::Float64 = 5.0 # Sigma multiplier for preclustering distance threshold maxframegap::Int = 5 # Maximum frame gap for temporal adjacency @@ -46,16 +46,16 @@ Configuration parameters for frame connection. Use with `frameconnect(smld, conf **Example:** ```julia # Create config with custom settings -config = ConnectConfig(maxframegap=10, nsigmadev=3.0) +config = FrameConnectConfig(maxframegap=10, nsigmadev=3.0) (combined, info) = frameconnect(smld, config) # Equivalent kwargs form (combined, info) = frameconnect(smld; maxframegap=10, nsigmadev=3.0) ``` -### ConnectInfo{T} +### FrameConnectInfo{T} ```julia -struct ConnectInfo{T} +struct FrameConnectInfo{T} connected::BasicSMLD{T} # Input with track_id assigned (uncombined) n_input::Int # Number of input localizations n_tracks::Int # Number of tracks formed diff --git a/src/SMLMFrameConnection.jl b/src/SMLMFrameConnection.jl index 29047bc..b282f8d 100644 --- a/src/SMLMFrameConnection.jl +++ b/src/SMLMFrameConnection.jl @@ -9,7 +9,7 @@ using StatsBase using Statistics export frameconnect, defineidealFC, combinelocalizations -export ConnectConfig, ConnectInfo, ParamStruct +export FrameConnectConfig, FrameConnectInfo, ParamStruct include("structdefinitions.jl") include("connectinfo.jl") diff --git a/src/connectinfo.jl b/src/connectinfo.jl index 340205a..b68cbf1 100644 --- a/src/connectinfo.jl +++ b/src/connectinfo.jl @@ -1,7 +1,7 @@ -# ConnectInfo struct for tuple-pattern return +# FrameConnectInfo struct for tuple-pattern return """ - ConnectInfo{T} + FrameConnectInfo{T} Secondary output from `frameconnect()` containing track assignments and algorithm metadata. @@ -36,7 +36,7 @@ println("Formed \$(info.n_tracks) tracks in \$(info.elapsed_s)s") # Access track assignments via info.connected ``` """ -struct ConnectInfo{T} <: AbstractSMLMInfo +struct FrameConnectInfo{T} <: AbstractSMLMInfo connected::BasicSMLD{T} n_input::Int n_tracks::Int diff --git a/src/frameconnect.jl b/src/frameconnect.jl index 48cfa6e..f4a534d 100644 --- a/src/frameconnect.jl +++ b/src/frameconnect.jl @@ -1,7 +1,7 @@ using SMLMData """ - (combined, info) = frameconnect(smld::BasicSMLD, config::ConnectConfig) + (combined, info) = frameconnect(smld::BasicSMLD, config::FrameConnectConfig) (combined, info) = frameconnect(smld::BasicSMLD; kwargs...) Connect repeated localizations of the same emitter in `smld`. @@ -17,9 +17,9 @@ using their MLE position estimate assuming Gaussian noise. # Arguments - `smld::BasicSMLD`: Localizations to connect. Must contain emitters with valid position uncertainties (σ_x, σ_y). -- `config::ConnectConfig`: Configuration parameters (optional, can use kwargs instead) +- `config::FrameConnectConfig`: Configuration parameters (optional, can use kwargs instead) -# Keyword Arguments (equivalent to ConnectConfig fields) +# Keyword Arguments (equivalent to FrameConnectConfig fields) - `nnearestclusters::Int=2`: Number of nearest preclusters used for local density estimates (see `estimatedensities`) - `nsigmadev::Float64=5.0`: Multiplier of localization errors that defines a @@ -32,7 +32,7 @@ using their MLE position estimate assuming Gaussian noise. # Returns A tuple `(combined, info)`: - `combined::BasicSMLD`: Connected localizations combined into higher precision results -- `info::ConnectInfo`: Track assignments and algorithm metadata (see [`ConnectInfo`](@ref)) +- `info::FrameConnectInfo`: Track assignments and algorithm metadata (see [`FrameConnectInfo`](@ref)) # Example ```julia @@ -41,7 +41,7 @@ A tuple `(combined, info)`: (combined, info) = frameconnect(smld; maxframegap=10) # Using config struct -config = ConnectConfig(maxframegap=10, nsigmadev=3.0) +config = FrameConnectConfig(maxframegap=10, nsigmadev=3.0) (combined, info) = frameconnect(smld, config) println("Connected \$(info.n_input) → \$(info.n_combined) localizations") @@ -53,11 +53,11 @@ track_ids = [e.track_id for e in info.connected.emitters] """ function frameconnect(smld::BasicSMLD{T,E}; kwargs...) where {T, E<:SMLMData.AbstractEmitter} # kwargs form forwards to config form - config = ConnectConfig(; kwargs...) + config = FrameConnectConfig(; kwargs...) return frameconnect(smld, config) end -function frameconnect(smld::BasicSMLD{T,E}, config::ConnectConfig) where {T, E<:SMLMData.AbstractEmitter} +function frameconnect(smld::BasicSMLD{T,E}, config::FrameConnectConfig) where {T, E<:SMLMData.AbstractEmitter} t_start = time() # Prepare a ParamStruct to keep track of parameters used. @@ -109,9 +109,9 @@ function frameconnect(smld::BasicSMLD{T,E}, config::ConnectConfig) where {T, E<: elapsed_s = time() - t_start - # Build ConnectInfo + # Build FrameConnectInfo n_tracks = length(unique(connectID_final)) - info = ConnectInfo{T}( + info = FrameConnectInfo{T}( smld_connected, length(smld.emitters), n_tracks, diff --git a/src/structdefinitions.jl b/src/structdefinitions.jl index 4520d68..72336b4 100644 --- a/src/structdefinitions.jl +++ b/src/structdefinitions.jl @@ -1,7 +1,7 @@ # This file defines some struct types used in the FrameConnection package. """ - ConnectConfig + FrameConnectConfig Configuration parameters for frame connection algorithm. @@ -18,18 +18,18 @@ Configuration parameters for frame connection algorithm. # Example ```julia # Using default config -config = ConnectConfig() +config = FrameConnectConfig() (combined, info) = frameconnect(smld, config) # Custom config -config = ConnectConfig(maxframegap=10, nsigmadev=3.0) +config = FrameConnectConfig(maxframegap=10, nsigmadev=3.0) (combined, info) = frameconnect(smld, config) # Kwargs form (equivalent to Config form) (combined, info) = frameconnect(smld; maxframegap=10, nsigmadev=3.0) ``` """ -Base.@kwdef struct ConnectConfig <: AbstractSMLMConfig +Base.@kwdef struct FrameConnectConfig <: AbstractSMLMConfig nnearestclusters::Int = 2 nsigmadev::Float64 = 5.0 maxframegap::Int = 5 diff --git a/test/test_frameconnect.jl b/test/test_frameconnect.jl index 88703f3..e156f4e 100644 --- a/test/test_frameconnect.jl +++ b/test/test_frameconnect.jl @@ -8,9 +8,9 @@ (combined, info) = frameconnect(smld) @test combined isa BasicSMLD - @test info isa ConnectInfo + @test info isa FrameConnectInfo - # ConnectInfo fields + # FrameConnectInfo fields @test info.connected isa BasicSMLD @test info.n_input == length(smld.emitters) @test info.n_tracks > 0 @@ -167,11 +167,11 @@ @test info.elapsed_s < 60.0 end - @testset "ConnectInfo type parameter" begin + @testset "FrameConnectInfo type parameter" begin # Test with Float64 smld64 = make_single_molecule_smld() (_, info64) = frameconnect(smld64) - @test info64 isa ConnectInfo{Float64} + @test info64 isa FrameConnectInfo{Float64} # Test with Float32 emitters32 = [SMLMData.Emitter2DFit{Float32}( @@ -182,6 +182,6 @@ camera32 = SMLMData.IdealCamera(1:64, 1:64, 0.1f0) smld32 = BasicSMLD(emitters32, camera32, 1, 1, Dict{String,Any}()) (_, info32) = frameconnect(smld32) - @test info32 isa ConnectInfo{Float32} + @test info32 isa FrameConnectInfo{Float32} end end diff --git a/test/test_types.jl b/test/test_types.jl index 0b41d64..492e890 100644 --- a/test/test_types.jl +++ b/test/test_types.jl @@ -1,5 +1,5 @@ @testset "Types" begin - @testset "ConnectInfo" begin + @testset "FrameConnectInfo" begin # Create minimal test data emitters = [SMLMData.Emitter2DFit{Float64}( 5.0, 5.0, 1000.0, 10.0, 0.02, 0.02, 0.0, 10.0, 1.0, 1, 1, 1, 1 @@ -7,11 +7,11 @@ camera = SMLMData.IdealCamera(1:64, 1:64, 0.1) smld = BasicSMLD(emitters, camera, 1, 1, Dict{String,Any}()) - info = ConnectInfo{Float64}( + info = FrameConnectInfo{Float64}( smld, 10, 5, 5, 0.1, 0.5, 0.01, 0.05, [1.0, 2.0], 1.5, :lap, 3 ) - @test info isa ConnectInfo{Float64} + @test info isa FrameConnectInfo{Float64} @test info.connected === smld @test info.n_input == 10 @test info.n_tracks == 5 From c8f9c0e73a183fb7139b80db2f4363d12a98a3aa Mon Sep 17 00:00:00 2001 From: kalidke Date: Sat, 7 Feb 2026 10:26:25 -0700 Subject: [PATCH 08/11] Update SMLMData compat to 0.7, switch to registered version Remove dev/path dependency override. SMLMData now resolves from General registry as v0.7.0 (requires AbstractSMLMConfig/Info types). Co-Authored-By: Claude Opus 4.6 --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 6cb6df6..9e9e07f 100644 --- a/Project.toml +++ b/Project.toml @@ -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" From 68fffc60fbd63fb87c7bc4d2eb922d3755c9080b Mon Sep 17 00:00:00 2001 From: kalidke Date: Sun, 8 Feb 2026 15:29:08 -0700 Subject: [PATCH 09/11] Add CLAUDE.md with development guide and architecture overview Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..0ff0109 --- /dev/null +++ b/CLAUDE.md @@ -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 `nsigmadev * mean(σ)` distance and `maxframegap` 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 From 47845e05a52dbb8556066668903a3350a4f9380f Mon Sep 17 00:00:00 2001 From: kalidke Date: Sun, 8 Feb 2026 16:05:47 -0700 Subject: [PATCH 10/11] Rename struct fields to snake_case for ecosystem consistency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renames: nnearestclusters→n_density_neighbors, nsigmadev→max_sigma_dist, maxframegap→max_frame_gap, nmaxnn→max_neighbors, initialdensity→initial_density. Also removes redundant `using SMLMData` from doc examples (re-exported). Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 2 +- README.md | 192 +++++++++++--------------- api_overview.md | 34 ++--- docs/src/index.md | 29 ++-- examples/benchmark.jl | 28 ++-- examples/frameconnection_example.jl | 10 +- examples/fullscale_benchmark.jl | 8 +- examples/realistic_frameconnection.jl | 10 +- src/connectinfo.jl | 4 +- src/create_costmatrix.jl | 8 +- src/defineidealFC.jl | 22 +-- src/estimatedensities.jl | 16 +-- src/frameconnect.jl | 24 ++-- src/precluster.jl | 16 +-- src/structdefinitions.jl | 44 +++--- test/test_defineidealFC.jl | 14 +- test/test_frameconnect.jl | 14 +- test/test_types.jl | 26 ++-- 18 files changed, 233 insertions(+), 268 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0ff0109..4797afb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -31,7 +31,7 @@ Test files must `include("test/test_helpers.jl")` first since `runtests.jl` load `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 `nsigmadev * mean(σ)` distance and `maxframegap` frames. Stores cluster assignments in emitter `track_id` fields. +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. diff --git a/README.md b/README.md index aeaf1ec..91933da 100644 --- a/README.md +++ b/README.md @@ -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 @@ -21,33 +17,77 @@ Pkg.add("SMLMFrameConnection") ## Quick Start ```julia -using SMLMData, SMLMFrameConnection +using SMLMFrameConnection -# Run frame connection on your data +# Frame connection on localization data (combined, info) = frameconnect(smld) -# combined is the main output - higher precision localizations -# info contains track assignments and algorithm metadata +# 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 | + +```julia +# Keyword form (most common) +(combined, info) = frameconnect(smld; max_frame_gap=10, max_sigma_dist=3.0) + +# 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. -Input must be a `BasicSMLD` from [SMLMData.jl](https://github.com/JuliaSMLM/SMLMData.jl) with emitters containing position uncertainties. +## Output Format -**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) +`frameconnect()` returns `(combined::BasicSMLD, info::FrameConnectInfo)`. -**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) +| 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 | -## Complete Example +### Combination Method + +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 + +## 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) @@ -65,116 +105,40 @@ emitters = [ 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 (combined, info) = frameconnect(smld) -# Result: localizations connected based on spatial/temporal proximity -# Combined uncertainty: σ_combined ≈ σ_individual / √n_connected -println("$(info.n_input) → $(info.n_combined) localizations") -``` - -## Outputs Explained - -```julia -(combined, info) = 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 | -|--------|-------------|-------------| -| `combined` | **Main output.** Combined high-precision localizations | Standard analysis | -| `info.connected` | Original localizations with `track_id` populated | When you need per-frame data with connection labels | -| `info.n_tracks` | Number of tracks formed | Summary statistics | -| `info.elapsed_s` | Algorithm wall time in seconds | Performance monitoring | - -### FrameConnectInfo Fields - -| Field | Type | Description | -|-------|------|-------------| -| `connected` | `BasicSMLD` | Input with `track_id` assigned (localizations uncombined) | -| `n_input` | `Int` | Number of input localizations | -| `n_tracks` | `Int` | Number of tracks formed | -| `n_combined` | `Int` | Number of output localizations | -| `k_on` | `Float64` | Estimated on rate (1/frame) | -| `k_off` | `Float64` | Estimated off rate (1/frame) | -| `k_bleach` | `Float64` | Estimated bleach rate (1/frame) | -| `p_miss` | `Float64` | Probability of missed detection | -| `initialdensity` | `Vector{Float64}` | Density estimate per cluster (emitters/μm²) | -| `elapsed_s` | `Float64` | Wall time in seconds | -| `algorithm` | `Symbol` | Algorithm used (`:lap`) | -| `n_preclusters` | `Int` | Number of preclusters formed | - -## Configuration - -Two equivalent ways to configure `frameconnect`: - -### Keyword Arguments (most common) -```julia -(combined, info) = 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 -) -``` - -### Config Struct (for reusable/shareable settings) -```julia -config = FrameConnectConfig(maxframegap=10, nsigmadev=3.0) -(combined, info) = frameconnect(smld, config) -``` - -### FrameConnectConfig Fields - -| Field | Default | Description | -|-------|---------|-------------| -| `nnearestclusters` | 2 | Nearest preclusters for local density estimation | -| `nsigmadev` | 5.0 | Sigma multiplier for preclustering distance threshold | -| `maxframegap` | 5 | Maximum frame gap for temporal adjacency | -| `nmaxnn` | 2 | Maximum nearest-neighbors for precluster membership | - -**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 Photophysics - -The algorithm estimates fluorophore photophysics from your data, accessible via `info`: - -| 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. diff --git a/api_overview.md b/api_overview.md index 7860bdb..48ceb65 100644 --- a/api_overview.md +++ b/api_overview.md @@ -1,6 +1,6 @@ # SMLMFrameConnection API Overview -Frame-connection for 2D SMLM data: combines repeated localizations of blinking fluorophores into higher-precision localizations. +Frame-connection for 2D SMLM data: 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. ## Main Functions @@ -26,7 +26,7 @@ Combines emitters sharing the same `track_id` using MLE weighted mean. Use when ### defineidealFC ```julia -defineidealFC(smld::BasicSMLD; maxframegap::Int=5) -> (smld_connected, smld_combined) +(smld_connected, smld_combined) = defineidealFC(smld::BasicSMLD; max_frame_gap::Int=5) ``` For simulated data where `track_id` indicates ground-truth emitter ID. Useful for validation/benchmarking. @@ -34,11 +34,11 @@ For simulated data where `track_id` indicates ground-truth emitter ID. Useful fo ### FrameConnectConfig ```julia -@kwdef struct FrameConnectConfig - nnearestclusters::Int = 2 # Nearest preclusters for local density estimation - nsigmadev::Float64 = 5.0 # Sigma multiplier for preclustering distance threshold - maxframegap::Int = 5 # Maximum frame gap for temporal adjacency - nmaxnn::Int = 2 # Maximum nearest-neighbors for precluster membership +@kwdef struct FrameConnectConfig <: AbstractSMLMConfig + n_density_neighbors::Int = 2 # Nearest preclusters for local density estimation + max_sigma_dist::Float64 = 5.0 # Sigma multiplier for preclustering distance threshold + max_frame_gap::Int = 5 # Maximum frame gap for temporal adjacency + max_neighbors::Int = 2 # Maximum nearest-neighbors for precluster membership end ``` Configuration parameters for frame connection. Use with `frameconnect(smld, config)` or pass fields as kwargs to `frameconnect(smld; kwargs...)`. @@ -46,16 +46,16 @@ Configuration parameters for frame connection. Use with `frameconnect(smld, conf **Example:** ```julia # Create config with custom settings -config = FrameConnectConfig(maxframegap=10, nsigmadev=3.0) +config = FrameConnectConfig(max_frame_gap=10, max_sigma_dist=3.0) (combined, info) = frameconnect(smld, config) # Equivalent kwargs form -(combined, info) = frameconnect(smld; maxframegap=10, nsigmadev=3.0) +(combined, info) = frameconnect(smld; max_frame_gap=10, max_sigma_dist=3.0) ``` ### FrameConnectInfo{T} ```julia -struct FrameConnectInfo{T} +struct FrameConnectInfo{T} <: AbstractSMLMInfo connected::BasicSMLD{T} # Input with track_id assigned (uncombined) n_input::Int # Number of input localizations n_tracks::Int # Number of tracks formed @@ -64,7 +64,7 @@ struct FrameConnectInfo{T} k_off::Float64 # Rate: visible → dark (1/frame) k_bleach::Float64 # Rate: photobleaching (1/frame) p_miss::Float64 # Probability of missed detection - initialdensity::Vector{Float64} # Emitter density per cluster (emitters/μm²) + initial_density::Vector{Float64} # Emitter density per cluster (emitters/μm²) elapsed_s::Float64 # Wall time in seconds algorithm::Symbol # Algorithm used (:lap) n_preclusters::Int # Number of preclusters formed @@ -80,15 +80,15 @@ track_ids = [e.track_id for e in info.connected.emitters] ### ParamStruct ```julia mutable struct ParamStruct - initialdensity::Vector{Float64} # Emitter density per cluster (emitters/μm²) - nnearestclusters::Int # Clusters for density estimation + initial_density::Vector{Float64} # Emitter density per cluster (emitters/μm²) + n_density_neighbors::Int # Clusters for density estimation k_on::Float64 # Rate: dark → visible (1/frame) k_off::Float64 # Rate: visible → dark (1/frame) k_bleach::Float64 # Rate: photobleaching (1/frame) p_miss::Float64 # Probability of missing localization when on - nsigmadev::Float64 # Preclustering threshold multiplier - maxframegap::Int # Max frame gap in preclusters - nmaxnn::Int # Max nearest-neighbors for preclustering + max_sigma_dist::Float64 # Preclustering threshold multiplier + max_frame_gap::Int # Max frame gap in preclusters + max_neighbors::Int # Max nearest-neighbors for preclustering end ``` @@ -123,7 +123,7 @@ Output `combined` contains emitters with: ## Dependencies -- SMLMData.jl (v0.6+): BasicSMLD, Emitter types +- SMLMData.jl (v0.7): BasicSMLD, Emitter types, AbstractSMLMConfig, AbstractSMLMInfo - Hungarian.jl: Linear assignment problem solver - NearestNeighbors.jl: Spatial clustering - Optim.jl: Parameter estimation diff --git a/docs/src/index.md b/docs/src/index.md index b3165e2..368acf5 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -4,7 +4,7 @@ CurrentModule = SMLMFrameConnection # SMLMFrameConnection -Frame-connection for 2D single molecule localization microscopy (SMLM) data. Combines repeated localizations of blinking fluorophores into higher-precision localizations using the algorithm from [Schodt & Lidke 2021](https://doi.org/10.3389/fbinf.2021.724325). +Frame-connection for 2D single molecule localization microscopy (SMLM) data: 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. See [Schodt & Lidke 2021](https://doi.org/10.3389/fbinf.2021.724325). ## Installation @@ -16,12 +16,13 @@ Pkg.add("SMLMFrameConnection") ## Quick Start ```julia -using SMLMData, SMLMFrameConnection +using SMLMFrameConnection # Run frame connection on your BasicSMLD with Emitter2DFit emitters -smld_connected, smld_preclustered, smld_combined, params = frameconnect(smld) +(combined, info) = frameconnect(smld) -# smld_combined is the main output - higher precision localizations +# combined is the main output - higher precision localizations +# info contains track assignments and algorithm metadata ``` ## Input Requirements @@ -48,24 +49,24 @@ Input `BasicSMLD` must contain `Emitter2DFit` emitters with: | Output | Description | |--------|-------------| -| `smld_combined` | **Main output** - combined high-precision localizations | -| `smld_connected` | Original localizations with `track_id` labels | -| `smld_preclustered` | Intermediate result (debugging) | -| `params` | Estimated photophysics parameters | +| `combined` | **Main output** - combined high-precision localizations | +| `info.connected` | Original localizations with `track_id` assigned | +| `info.n_tracks` | Number of tracks formed | +| `info.elapsed_s` | Wall time in seconds | ## Parameters ```julia frameconnect(smld; - nnearestclusters = 2, # Clusters for density estimation - nsigmadev = 5.0, # Distance threshold multiplier - maxframegap = 5, # Max frame gap for connections - nmaxnn = 2 # Nearest neighbors for preclustering + n_density_neighbors = 2, # Clusters for density estimation + max_sigma_dist = 5.0, # Distance threshold multiplier + max_frame_gap = 5, # Max frame gap for connections + max_neighbors = 2 # Nearest neighbors for preclustering ) ``` -- `nsigmadev`: Higher values allow connections over larger distances -- `maxframegap`: Increase for dyes with long dark states (dSTORM: 10-20) +- `max_sigma_dist`: Higher values allow connections over larger distances +- `max_frame_gap`: Increase for dyes with long dark states (dSTORM: 10-20) ## API Reference diff --git a/examples/benchmark.jl b/examples/benchmark.jl index e097b70..960671b 100644 --- a/examples/benchmark.jl +++ b/examples/benchmark.jl @@ -62,13 +62,13 @@ Benchmark a single run with timing and allocation tracking. function benchmark_single(smld; warmup::Bool = false) # Warmup run (if needed) if warmup - frameconnect(smld; nnearestclusters=1) + frameconnect(smld; n_density_neighbors=1) end # Timed run GC.gc() # Clean up before measurement - stats = @timed frameconnect(smld; nnearestclusters=2, nsigmadev=5.0, maxframegap=5, nmaxnn=2) + stats = @timed frameconnect(smld; n_density_neighbors=2, max_sigma_dist=5.0, max_frame_gap=5, max_neighbors=2) (combined, info) = stats.value return ( @@ -157,7 +157,7 @@ function profile_detailed(; n_molecules::Int = 50, n_frames::Int = 100) println("Input localizations: $(length(smld.emitters))") # Warmup - frameconnect(smld; nnearestclusters=1) + frameconnect(smld; n_density_neighbors=1) println("\nRunning with @time macro:") println("-" ^ 70) @@ -165,10 +165,10 @@ function profile_detailed(; n_molecules::Int = 50, n_frames::Int = 100) GC.gc() @time (combined, info) = frameconnect( smld; - nnearestclusters = 2, - nsigmadev = 5.0, - maxframegap = 5, - nmaxnn = 2 + n_density_neighbors = 2, + max_sigma_dist = 5.0, + max_frame_gap = 5, + max_neighbors = 2 ) println("\nOutput:") @@ -196,14 +196,14 @@ function allocation_breakdown(; n_molecules::Int = 30, n_frames::Int = 50) println("\nDataset: $(length(smld.emitters)) localizations") # Warmup - frameconnect(smld; nnearestclusters=1) + frameconnect(smld; n_density_neighbors=1) # Prepare params params = SMLMFrameConnection.ParamStruct() - params.nnearestclusters = 2 - params.nsigmadev = 5.0 - params.maxframegap = 5 - params.nmaxnn = 2 + params.n_density_neighbors = 2 + params.max_sigma_dist = 5.0 + params.max_frame_gap = 5 + params.max_neighbors = 2 println("\n@allocated for each step:") println("-" ^ 50) @@ -239,9 +239,9 @@ function allocation_breakdown(; n_molecules::Int = 30, n_frames::Int = 50) # Step 4: Estimate densities GC.gc() alloc_densities = @allocated begin - params.initialdensity = SMLMFrameConnection.estimatedensities(smld_pre, clusterdata, params) + params.initial_density = SMLMFrameConnection.estimatedensities(smld_pre, clusterdata, params) end - params.initialdensity = SMLMFrameConnection.estimatedensities(smld_pre, clusterdata, params) + params.initial_density = SMLMFrameConnection.estimatedensities(smld_pre, clusterdata, params) @printf(" estimatedensities: %10.2f KB\n", alloc_densities / 1024) # Step 5: Connect localizations (LAP) diff --git a/examples/frameconnection_example.jl b/examples/frameconnection_example.jl index b6c52bc..8f0d718 100644 --- a/examples/frameconnection_example.jl +++ b/examples/frameconnection_example.jl @@ -97,10 +97,10 @@ println(" True molecules: $(length(unique(e.track_id for e in smld_truth.emitte println("\nRunning frame connection...") (combined, info) = frameconnect( smld_input; - nnearestclusters = 2, - nsigmadev = 5.0, - maxframegap = 5, - nmaxnn = 2 + n_density_neighbors = 2, + max_sigma_dist = 5.0, + max_frame_gap = 5, + max_neighbors = 2 ) println(" Combined localizations: $(length(combined.emitters))") @@ -108,7 +108,7 @@ println(" Time: $(info.elapsed_s) seconds") # Compare with ideal result (using ground truth track_id) println("\nComputing ideal frame connection (ground truth)...") -smld_ideal_connected, smld_ideal_combined = defineidealFC(smld_truth; maxframegap = 5) +smld_ideal_connected, smld_ideal_combined = defineidealFC(smld_truth; max_frame_gap = 5) println(" Ideal combined: $(length(smld_ideal_combined.emitters))") # Print estimated parameters diff --git a/examples/fullscale_benchmark.jl b/examples/fullscale_benchmark.jl index ac4153d..d17108f 100644 --- a/examples/fullscale_benchmark.jl +++ b/examples/fullscale_benchmark.jl @@ -83,10 +83,10 @@ function benchmark_single_dataset(smld::BasicSMLD) stats = @timed begin frameconnect( smld; - nnearestclusters = 2, - nsigmadev = 5.0, - maxframegap = 10, - nmaxnn = 2 + n_density_neighbors = 2, + max_sigma_dist = 5.0, + max_frame_gap = 10, + max_neighbors = 2 ) end diff --git a/examples/realistic_frameconnection.jl b/examples/realistic_frameconnection.jl index e7d72f3..eab8847 100644 --- a/examples/realistic_frameconnection.jl +++ b/examples/realistic_frameconnection.jl @@ -119,10 +119,10 @@ function main() stats = @timed begin frameconnect( smld_input; - nnearestclusters = 2, - nsigmadev = 5.0, - maxframegap = 10, # Allow gaps since k_on=0.03 means reblinking - nmaxnn = 2 + n_density_neighbors = 2, + max_sigma_dist = 5.0, + max_frame_gap = 10, # Allow gaps since k_on=0.03 means reblinking + max_neighbors = 2 ) end @@ -187,7 +187,7 @@ function main() println("-" ^ 70) # Use original smld_noisy which has true track_id from simulation - smld_ideal_connected, smld_ideal_combined = defineidealFC(smld_noisy; maxframegap = 10) + smld_ideal_connected, smld_ideal_combined = defineidealFC(smld_noisy; max_frame_gap = 10) n_ideal = length(smld_ideal_combined.emitters) @printf(" True emitters (SMLMSim): %d\n", n_true_emitters) diff --git a/src/connectinfo.jl b/src/connectinfo.jl index b68cbf1..b98474e 100644 --- a/src/connectinfo.jl +++ b/src/connectinfo.jl @@ -14,7 +14,7 @@ Secondary output from `frameconnect()` containing track assignments and algorith - `k_off::Float64`: Estimated off rate (1/frame) - `k_bleach::Float64`: Estimated bleach rate (1/frame) - `p_miss::Float64`: Probability of missed detection -- `initialdensity::Vector{Float64}`: Initial density estimate per cluster (emitters/μm²) +- `initial_density::Vector{Float64}`: Initial density estimate per cluster (emitters/μm²) - `elapsed_s::Float64`: Wall time in seconds - `algorithm::Symbol`: Algorithm used (`:lap`) - `n_preclusters::Int`: Number of preclusters formed @@ -45,7 +45,7 @@ struct FrameConnectInfo{T} <: AbstractSMLMInfo k_off::Float64 k_bleach::Float64 p_miss::Float64 - initialdensity::Vector{Float64} + initial_density::Vector{Float64} elapsed_s::Float64 algorithm::Symbol n_preclusters::Int diff --git a/src/create_costmatrix.jl b/src/create_costmatrix.jl index 656aa38..187f912 100644 --- a/src/create_costmatrix.jl +++ b/src/create_costmatrix.jl @@ -31,8 +31,8 @@ function create_costmatrix(clusterdata::Vector{Matrix{Float32}}, k_off = params.k_off k_bleach = params.k_bleach p_miss = params.p_miss - rho_0 = params.initialdensity[clusterind] - maxframegap = params.maxframegap + rho_0 = params.initial_density[clusterind] + max_frame_gap = params.max_frame_gap # Populate the upper-left "connection" block. nlocalizations = length(framenum) @@ -89,8 +89,8 @@ function create_costmatrix(clusterdata::Vector{Matrix{Float32}}, indices = 1:nlocalizations framesint = Int32.(framenum) for nn in indices - deltaframe_past = minimum([maxframegap; framesint[nn]-startframe]) - deltaframe_future = minimum([maxframegap; nframes-framesint[nn]]) + deltaframe_past = minimum([max_frame_gap; framesint[nn]-startframe]) + deltaframe_future = minimum([max_frame_gap; nframes-framesint[nn]]) # Localization uncertainty ellipse area (μm²) to make density dimensionless # Area = π√det(Σ) where det(Σ) = σ_x²σ_y² - σ_xy² det_loc = x_se[nn]^2 * y_se[nn]^2 - xy_cov[nn]^2 diff --git a/src/defineidealFC.jl b/src/defineidealFC.jl index c41271d..abc73ba 100644 --- a/src/defineidealFC.jl +++ b/src/defineidealFC.jl @@ -3,7 +3,7 @@ using SMLMData """ connect1DS(smld::BasicSMLD{T,E}, dataset::Int, connectID::Vector{Int}, maxID::Int; - maxframegap::Int = 5) where {T, E<:SMLMData.AbstractEmitter} + max_frame_gap::Int = 5) where {T, E<:SMLMData.AbstractEmitter} Define the "ideal" frame-connection result for simulated `smld` with one dataset. @@ -17,7 +17,7 @@ call defineidealFC(). - `dataset`: Dataset number to be connected. - `connectID`: Current connection IDs for all emitters. - `maxID`: Current maximum ID value. -- `maxframegap`: Maximum frame gap allowed between localizations connected in +- `max_frame_gap`: Maximum frame gap allowed between localizations connected in the "ideal" result. # Returns @@ -25,7 +25,7 @@ call defineidealFC(). """ function connect1DS(smld::BasicSMLD{T,E}, dataset::Int, connectID::Vector{Int}, maxID::Int; - maxframegap::Int = 5) where {T, E<:SMLMData.AbstractEmitter} + max_frame_gap::Int = 5) where {T, E<:SMLMData.AbstractEmitter} emitters = smld.emitters # Find indices for current dataset @@ -58,13 +58,13 @@ function connect1DS(smld::BasicSMLD{T,E}, dataset::Int, # If all of these localizations are within the framegap, no action is # needed (they already share the same connectID). framediff = diff(sortedframes) - if all(framediff .<= maxframegap) + if all(framediff .<= max_frame_gap) continue end # Determine which localizations we can combine. for ff = 1:length(framediff) - if framediff[ff] <= maxframegap + if framediff[ff] <= max_frame_gap # Connect these localizations. connectID_DS[sorted_local_inds[ff+1]] = connectID_DS[sorted_local_inds[ff]] @@ -85,7 +85,7 @@ end """ smld_connected, smld_combined = defineidealFC( smld::BasicSMLD{T,E}; - maxframegap::Int = 5) where {T, E<:SMLMData.AbstractEmitter} + max_frame_gap::Int = 5) where {T, E<:SMLMData.AbstractEmitter} Define the "ideal" frame-connection result for a simulated `smld`. @@ -94,15 +94,15 @@ This function defines the "ideal" frame connection result from a simulation. That is to say, for a simulated BasicSMLD structure `smld` with `track_id` field populated to indicate emitter membership of localizations, this function will generate an "ideal" FC result which combines all blinking events that appeared -with frame gaps less than `maxframegap` of one another. Note that for very +with frame gaps less than `max_frame_gap` of one another. Note that for very high duty cycles, multiple blinking events might be mistakingly combined by -this method (i.e., if the emitter blinks back on within `maxframegap` frames +this method (i.e., if the emitter blinks back on within `max_frame_gap` frames of its previous blink). Note that localizations are not allowed to be connected across datasets. # Inputs - `smld`: BasicSMLD with track_id populated to indicate emitter ID. -- `maxframegap`: Maximum frame gap allowed between localizations connected in +- `max_frame_gap`: Maximum frame gap allowed between localizations connected in the "ideal" result. # Outputs @@ -111,7 +111,7 @@ connected across datasets. - `smld_combined`: Ideal frame-connection result with localizations combined. """ function defineidealFC(smld::BasicSMLD{T,E}; - maxframegap::Int = 5) where {T, E<:SMLMData.AbstractEmitter} + max_frame_gap::Int = 5) where {T, E<:SMLMData.AbstractEmitter} emitters = smld.emitters # Initialize connectID from track_id @@ -123,7 +123,7 @@ function defineidealFC(smld::BasicSMLD{T,E}; # Loop through datasets and combine localizations as appropriate. for ds in datasets - connectID, maxID = connect1DS(smld, ds, connectID, maxID; maxframegap = maxframegap) + connectID, maxID = connect1DS(smld, ds, connectID, maxID; max_frame_gap = max_frame_gap) end # Compress connectID diff --git a/src/estimatedensities.jl b/src/estimatedensities.jl index 72250a7..23d2cb4 100644 --- a/src/estimatedensities.jl +++ b/src/estimatedensities.jl @@ -2,13 +2,13 @@ using SMLMData using NearestNeighbors """ - initialdensity = estimatedensities(smld::BasicSMLD{T,E}, + initial_density = estimatedensities(smld::BasicSMLD{T,E}, clusterdata::Vector{Matrix{Float32}}, params::ParamStruct) where {T, E<:SMLMData.AbstractEmitter} Estimate local emitter densities for clusters in `smld` and `clusterdata`. # Description -The initial local densities `initialdensity` around each pre-cluster present in +The initial local densities `initial_density` around each pre-cluster present in `smld`/`clusterdata` are estimated based on the local density of pre-clusters throughout the entire set of data as well as some of the rate parameters provided in `params`. @@ -31,15 +31,15 @@ function estimatedensities(smld::BasicSMLD{T,E}, # If only one cluster is present, we should return an answer right away and stop. if nclusters == 1 if size(clusterdata[1], 1) == 1 - initialdensity = 1.0 + initial_density = 1.0 else clusterarea = (maximum(clusterdata[1][:, 1]) - minimum(clusterdata[1][:, 1])) * (maximum(clusterdata[1][:, 2]) - minimum(clusterdata[1][:, 2])) - initialdensity = (1 / clusterarea) * + initial_density = (1 / clusterarea) * ((params.k_bleach / params.k_off) / (1 - params.p_miss)) / (1 - exp(-params.k_bleach * dutycycle * (maxframe - 1))) end - return [initialdensity] # Return as vector to match ParamStruct.initialdensity type + return [initial_density] # Return as vector to match ParamStruct.initial_density type end # Determine the center of all clusters assuming each arose from the same @@ -79,7 +79,7 @@ function estimatedensities(smld::BasicSMLD{T,E}, # Estimate the local cluster density based on the distance to # nearest-neighbors. - kneighbors = minimum([params.nnearestclusters; nclusters - 1]) + kneighbors = minimum([params.n_density_neighbors; nclusters - 1]) kneighbors = max(kneighbors, 1) # Ensure at least 1 neighbor # Ensure we don't request more neighbors than available (including self) kneighbors = min(kneighbors, nclusters - 1) @@ -109,10 +109,10 @@ function estimatedensities(smld::BasicSMLD{T,E}, # Estimate the density of underlying emitters based on cluster density. lambda1 = params.k_bleach * dutycycle lambda2 = (params.k_on + params.k_off + params.k_bleach) - lambda1 - initialdensity = clusterdensity * + initial_density = clusterdensity * (1.0 / dutycycle) * (1.0 / params.k_off) * (1.0 / (1.0 - params.p_miss)) ./ ((1.0 / lambda1) * (1.0 - exp(-lambda1 * (maxframe - 1.0))) - (1.0 / lambda2) * (1.0 - exp(-lambda2 * (maxframe - 1.0)))) - return initialdensity + return initial_density end diff --git a/src/frameconnect.jl b/src/frameconnect.jl index f4a534d..b697926 100644 --- a/src/frameconnect.jl +++ b/src/frameconnect.jl @@ -20,13 +20,13 @@ using their MLE position estimate assuming Gaussian noise. - `config::FrameConnectConfig`: Configuration parameters (optional, can use kwargs instead) # Keyword Arguments (equivalent to FrameConnectConfig fields) -- `nnearestclusters::Int=2`: Number of nearest preclusters used for local density +- `n_density_neighbors::Int=2`: Number of nearest preclusters used for local density estimates (see `estimatedensities`) -- `nsigmadev::Float64=5.0`: Multiplier of localization errors that defines a +- `max_sigma_dist::Float64=5.0`: Multiplier of localization errors that defines a pre-clustering distance threshold (see `precluster`) -- `maxframegap::Int=5`: Maximum frame gap between temporally adjacent localizations +- `max_frame_gap::Int=5`: Maximum frame gap between temporally adjacent localizations in a precluster (see `precluster`) -- `nmaxnn::Int=2`: Maximum number of nearest-neighbors inspected for precluster +- `max_neighbors::Int=2`: Maximum number of nearest-neighbors inspected for precluster membership (see `precluster`) # Returns @@ -38,10 +38,10 @@ A tuple `(combined, info)`: ```julia # Using kwargs (most common) (combined, info) = frameconnect(smld) -(combined, info) = frameconnect(smld; maxframegap=10) +(combined, info) = frameconnect(smld; max_frame_gap=10) # Using config struct -config = FrameConnectConfig(maxframegap=10, nsigmadev=3.0) +config = FrameConnectConfig(max_frame_gap=10, max_sigma_dist=3.0) (combined, info) = frameconnect(smld, config) println("Connected \$(info.n_input) → \$(info.n_combined) localizations") @@ -62,10 +62,10 @@ function frameconnect(smld::BasicSMLD{T,E}, config::FrameConnectConfig) where {T # Prepare a ParamStruct to keep track of parameters used. params = ParamStruct() - params.nnearestclusters = config.nnearestclusters - params.nsigmadev = config.nsigmadev - params.maxframegap = config.maxframegap - params.nmaxnn = config.nmaxnn + params.n_density_neighbors = config.n_density_neighbors + params.max_sigma_dist = config.max_sigma_dist + params.max_frame_gap = config.max_frame_gap + params.max_neighbors = config.max_neighbors # Generate pre-clusters of localizations in `smld`. smld_preclustered = precluster(smld, params) @@ -77,7 +77,7 @@ function frameconnect(smld::BasicSMLD{T,E}, config::FrameConnectConfig) where {T estimateparams(smld_preclustered, clusterdata) # Estimate the underlying density of emitters. - params.initialdensity = + params.initial_density = estimatedensities(smld_preclustered, clusterdata, params) # Get nframes @@ -120,7 +120,7 @@ function frameconnect(smld::BasicSMLD{T,E}, config::FrameConnectConfig) where {T params.k_off, params.k_bleach, params.p_miss, - params.initialdensity, + params.initial_density, elapsed_s, :lap, n_preclusters diff --git a/src/precluster.jl b/src/precluster.jl index 4f2e5b7..8938d59 100644 --- a/src/precluster.jl +++ b/src/precluster.jl @@ -11,8 +11,8 @@ Cluster localizations in `smld` based on distance and time thresholds in `params # Description Localizations in the input structure `smld` are clustered together based on their spatiotemporal separations. All localizations within a spatial -threshold of `params.nsigmadev*mean([σ_x σ_y])` and a temporal -threshold of `params.maxframegap` of one another will be clustered together, +threshold of `params.max_sigma_dist*mean([σ_x σ_y])` and a temporal +threshold of `params.max_frame_gap` of one another will be clustered together, meaning that these localizations now share the same unique integer value for their track_id field. @@ -46,9 +46,9 @@ function precluster(smld::BasicSMLD{T,E}, mean_se = Statistics.mean([σ_x σ_y]; dims = 2) # Isolate some parameters from params. - maxframegap = params.maxframegap - nsigmadev = params.nsigmadev - nmaxnn = params.nmaxnn + max_frame_gap = params.max_frame_gap + max_sigma_dist = params.max_sigma_dist + max_neighbors = params.max_neighbors # Initialize a connectID array, with each localization being considered # a unique cluster. @@ -81,7 +81,7 @@ function precluster(smld::BasicSMLD{T,E}, # Determine which localizations should be considered for # clustering. currentindFN = (1:nperframe[ff]) .+ ncumulativeFN[ff] - candidateind = findall((framenumCDS .>= (frames[ff] .- maxframegap)) .& + candidateind = findall((framenumCDS .>= (frames[ff] .- max_frame_gap)) .& (framenumCDS .<= frames[ff])) if length(candidateind) < 2 maxID += 1 @@ -94,14 +94,14 @@ function precluster(smld::BasicSMLD{T,E}, # which is a candidate for clustering. kdtree = NearestNeighbors.KDTree(xyCDS[:, candidateind]) nnindices, nndist = NearestNeighbors.knn(kdtree, xyCDS[:, currentindFN], - min(nmaxnn + 1, length(candidateind)), true) + min(max_neighbors + 1, length(candidateind)), true) # Assign localizations to clusters based on `nndist`. for ii in 1:nperframe[ff] # Determine which candidates meet our distance cutoff. se_sum = mean_seCDS[currentindFN[ii]] .+ mean_seCDS[candidateind[nnindices[ii]]] - validnninds = nnindices[ii][nndist[ii].<=(nsigmadev*se_sum)] + validnninds = nnindices[ii][nndist[ii].<=(max_sigma_dist*se_sum)] # Update connectIDCDS to reflect the new clusters. updateinds = unique([currentindFN[ii] diff --git a/src/structdefinitions.jl b/src/structdefinitions.jl index 72336b4..caf3bd0 100644 --- a/src/structdefinitions.jl +++ b/src/structdefinitions.jl @@ -6,13 +6,13 @@ Configuration parameters for frame connection algorithm. # Fields -- `nnearestclusters::Int=2`: Number of nearest preclusters used for local density +- `n_density_neighbors::Int=2`: Number of nearest preclusters used for local density estimates (see `estimatedensities`) -- `nsigmadev::Float64=5.0`: Multiplier of localization errors that defines a +- `max_sigma_dist::Float64=5.0`: Multiplier of localization errors that defines a pre-clustering distance threshold (see `precluster`) -- `maxframegap::Int=5`: Maximum frame gap between temporally adjacent localizations +- `max_frame_gap::Int=5`: Maximum frame gap between temporally adjacent localizations in a precluster (see `precluster`) -- `nmaxnn::Int=2`: Maximum number of nearest-neighbors inspected for precluster +- `max_neighbors::Int=2`: Maximum number of nearest-neighbors inspected for precluster membership (see `precluster`) # Example @@ -22,41 +22,41 @@ config = FrameConnectConfig() (combined, info) = frameconnect(smld, config) # Custom config -config = FrameConnectConfig(maxframegap=10, nsigmadev=3.0) +config = FrameConnectConfig(max_frame_gap=10, max_sigma_dist=3.0) (combined, info) = frameconnect(smld, config) # Kwargs form (equivalent to Config form) -(combined, info) = frameconnect(smld; maxframegap=10, nsigmadev=3.0) +(combined, info) = frameconnect(smld; max_frame_gap=10, max_sigma_dist=3.0) ``` """ Base.@kwdef struct FrameConnectConfig <: AbstractSMLMConfig - nnearestclusters::Int = 2 - nsigmadev::Float64 = 5.0 - maxframegap::Int = 5 - nmaxnn::Int = 2 + n_density_neighbors::Int = 2 + max_sigma_dist::Float64 = 5.0 + max_frame_gap::Int = 5 + max_neighbors::Int = 2 end """ - ParamStruct(initialdensity::Vector{Float64}, + ParamStruct(initial_density::Vector{Float64}, k_on::Float64, k_off::Float64, k_bleach::Float64, p_miss::Float64, - nsigmadev::Float64, maxframegap::Int, nnearestclusters::Int) + max_sigma_dist::Float64, max_frame_gap::Int, n_density_neighbors::Int) Structure of parameters needed for frame-connection. # Fields -- `initialdensity`: Density of emitters at the start of the experiment. +- `initial_density`: Density of emitters at the start of the experiment. (see estimatedensities()) (emitters/μm²) -- `nnearestclusters`: Number of nearest preclusters used for local density +- `n_density_neighbors`: Number of nearest preclusters used for local density estimates. (default = 2)(see estimatedensities()) - `k_on`: Rate at which dark emitters convert to the visible state (1/frame). - `k_off`: Rate at which visible emitters convert to the reversible dark state (1/frame). - `k_bleach`: Rate at which visible emitters are irreversibly photobleached (1/frame). - `p_miss`: Probability of missing a localization of a visible emitter. -- `nsigmadev`: Multiplier of localization errors that defines a pre-clustering +- `max_sigma_dist`: Multiplier of localization errors that defines a pre-clustering distance threshold. (default = 5)(see precluster())(unitless) -- `maxframegap`: Maximum frame gap between temporally adjacent localizations in +- `max_frame_gap`: Maximum frame gap between temporally adjacent localizations in a precluster. (default = 5)(see precluster())(frames) -- `nmaxnn`: Maximum number of nearest-neighbors inspected for precluster +- `max_neighbors`: Maximum number of nearest-neighbors inspected for precluster membership. Ideally, this would be set to inf, but that's not feasible for most data. (default = 2)(see precluster()) @@ -65,15 +65,15 @@ The duty cycle (fraction of time in ON state) is k_on/(k_on + k_off). For typica dSTORM, k_on << k_off gives low duty cycle (mostly dark, brief blinks). """ mutable struct ParamStruct - initialdensity::Vector{Float64} - nnearestclusters::Int + initial_density::Vector{Float64} + n_density_neighbors::Int k_on::Float64 k_off::Float64 k_bleach::Float64 p_miss::Float64 - nsigmadev::Float64 - maxframegap::Int - nmaxnn::Int + max_sigma_dist::Float64 + max_frame_gap::Int + max_neighbors::Int end ParamStruct() = ParamStruct([], 2, 0.0, 0.0, 0.0, 0.0, 5.0, 5, 2) diff --git a/test/test_defineidealFC.jl b/test/test_defineidealFC.jl index 6c73775..4f4cfac 100644 --- a/test/test_defineidealFC.jl +++ b/test/test_defineidealFC.jl @@ -18,14 +18,14 @@ ] smld = make_test_smld(emitters; n_frames=3) - smld_connected, smld_combined = defineidealFC(smld; maxframegap=5) + smld_connected, smld_combined = defineidealFC(smld; max_frame_gap=5) # Should have 2 combined localizations (one per unique emitter) @test length(smld_combined.emitters) == 2 end - @testset "respects maxframegap" begin - # Same emitter (track_id=1) but with frame gap > maxframegap + @testset "respects max_frame_gap" begin + # Same emitter (track_id=1) but with frame gap > max_frame_gap emitters = [ make_emitter(5.0, 5.0, 1; track_id=1), make_emitter(5.0, 5.0, 2; track_id=1), @@ -34,8 +34,8 @@ ] smld = make_test_smld(emitters; n_frames=11) - # With maxframegap=5, should split into 2 blinking events - smld_connected, smld_combined = defineidealFC(smld; maxframegap=5) + # With max_frame_gap=5, should split into 2 blinking events + smld_connected, smld_combined = defineidealFC(smld; max_frame_gap=5) @test length(smld_combined.emitters) == 2 end @@ -45,7 +45,7 @@ emitters = [make_emitter(5.0, 5.0, i; track_id=1) for i in 1:5] smld = make_test_smld(emitters; n_frames=5) - _, smld_combined = defineidealFC(smld; maxframegap=5) + _, smld_combined = defineidealFC(smld; max_frame_gap=5) @test length(smld_combined.emitters) == 1 end @@ -60,7 +60,7 @@ ] smld = make_test_smld(emitters; n_frames=4) - _, smld_combined = defineidealFC(smld; maxframegap=5) + _, smld_combined = defineidealFC(smld; max_frame_gap=5) # Should have 2 combined localizations (one per emitter identity) @test length(smld_combined.emitters) == 2 diff --git a/test/test_frameconnect.jl b/test/test_frameconnect.jl index e156f4e..f9ce90e 100644 --- a/test/test_frameconnect.jl +++ b/test/test_frameconnect.jl @@ -29,7 +29,7 @@ ) smld = make_test_smld(emitters; n_frames=5) - (combined, info) = frameconnect(smld; maxframegap=5, nnearestclusters=1) + (combined, info) = frameconnect(smld; max_frame_gap=5, n_density_neighbors=1) # Target molecule should be combined (may have 5 background singles) # At minimum, we should have fewer emitters than input @@ -48,7 +48,7 @@ ) smld = make_test_smld(emitters; n_frames=3) - (combined, info) = frameconnect(smld; maxframegap=5) + (combined, info) = frameconnect(smld; max_frame_gap=5) # Should combine into ~4 molecules (fewer than 12 input emitters) @test length(combined.emitters) <= 8 @@ -87,10 +87,10 @@ @test info.k_off >= 0 @test info.k_bleach >= 0 @test 0 <= info.p_miss <= 1 - @test !isempty(info.initialdensity) + @test !isempty(info.initial_density) end - @testset "respects maxframegap" begin + @testset "respects max_frame_gap" begin # Create molecules with gaps, plus background for density estimation emitters = vcat( make_blinking_molecule(5.0, 5.0, [1, 2, 3]), # Frames 1-3 @@ -100,8 +100,8 @@ ) smld = make_test_smld(emitters; n_frames=12) - # With maxframegap=5, the two groups at (5,5) should NOT connect - (combined, info) = frameconnect(smld; maxframegap=5) + # With max_frame_gap=5, the two groups at (5,5) should NOT connect + (combined, info) = frameconnect(smld; max_frame_gap=5) # Should have at least 3 combined localizations (2 for split molecule + 2 background) @test length(combined.emitters) >= 3 @@ -144,7 +144,7 @@ emitters = vcat(target_emitters, background) smld = make_test_smld(emitters; n_frames=n_locs) - (combined, info) = frameconnect(smld; maxframegap=n_locs, nnearestclusters=1) + (combined, info) = frameconnect(smld; max_frame_gap=n_locs, n_density_neighbors=1) # Find the combined target molecule (around position 5,5) target_combined = filter(e -> e.x < 10.0 && e.y < 10.0, combined.emitters) diff --git a/test/test_types.jl b/test/test_types.jl index 492e890..2d383c0 100644 --- a/test/test_types.jl +++ b/test/test_types.jl @@ -20,7 +20,7 @@ @test info.k_off == 0.5 @test info.k_bleach == 0.01 @test info.p_miss == 0.05 - @test info.initialdensity == [1.0, 2.0] + @test info.initial_density == [1.0, 2.0] @test info.elapsed_s == 1.5 @test info.algorithm == :lap @test info.n_preclusters == 3 @@ -30,15 +30,15 @@ @testset "default constructor" begin params = ParamStruct() @test params isa ParamStruct - @test params.initialdensity == [] - @test params.nnearestclusters == 2 + @test params.initial_density == [] + @test params.n_density_neighbors == 2 @test params.k_on == 0.0 @test params.k_off == 0.0 @test params.k_bleach == 0.0 @test params.p_miss == 0.0 - @test params.nsigmadev == 5.0 - @test params.maxframegap == 5 - @test params.nmaxnn == 2 + @test params.max_sigma_dist == 5.0 + @test params.max_frame_gap == 5 + @test params.max_neighbors == 2 end @testset "field mutation" begin @@ -51,21 +51,21 @@ params.k_off = 0.5 @test params.k_off == 0.5 - params.initialdensity = [1.0, 2.0] - @test params.initialdensity == [1.0, 2.0] + params.initial_density = [1.0, 2.0] + @test params.initial_density == [1.0, 2.0] end @testset "full constructor" begin params = ParamStruct([1.0], 3, 0.1, 0.5, 0.01, 0.05, 4.0, 10, 3) - @test params.initialdensity == [1.0] - @test params.nnearestclusters == 3 + @test params.initial_density == [1.0] + @test params.n_density_neighbors == 3 @test params.k_on == 0.1 @test params.k_off == 0.5 @test params.k_bleach == 0.01 @test params.p_miss == 0.05 - @test params.nsigmadev == 4.0 - @test params.maxframegap == 10 - @test params.nmaxnn == 3 + @test params.max_sigma_dist == 4.0 + @test params.max_frame_gap == 10 + @test params.max_neighbors == 3 end end end From f35ba9e393c1eba962ee5ed70033e42fda82ac0e Mon Sep 17 00:00:00 2001 From: kalidke Date: Sun, 8 Feb 2026 16:08:49 -0700 Subject: [PATCH 11/11] =?UTF-8?q?Update=20CI:=20actions/cache=20v1?= =?UTF-8?q?=E2=86=92v4,=20Julia=201.6=E2=86=921,=20checkout=20v4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit actions/cache v1 was deprecated by GitHub, causing immediate CI failures. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/CI.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index bf89f33..6c1d294 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -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: @@ -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: |